@kendoo.agentdesk/agentdesk 0.25.2 → 0.27.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 +4 -1
- package/README.md +1 -0
- package/bin/agentdesk.mjs +8 -0
- package/cli/daemon.mjs +76 -22
- package/cli/init.mjs +5 -2
- package/cli/login.mjs +41 -3
- package/cli/projects.mjs +41 -6
- package/cli/security-check.mjs +187 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,7 @@ 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
|
-
## [
|
|
11
|
+
## [0.27.0] — 2026-07-06
|
|
12
12
|
|
|
13
13
|
### Added
|
|
14
14
|
- `[UI]` Private-session sharing flow. When someone visits a session URL they don't own, they now see a friendly "Shh… this one's private" page with a one-click "Request access" button instead of a blank error. The session owner sees pending requests in a new header inbox and can grant viewer access for just that session or the whole project. Viewer-granted sessions show up in the teammate's sidebar tagged as a viewer.
|
|
@@ -16,6 +16,9 @@ Internal refactors, infrastructure changes, and architectural notes are not list
|
|
|
16
16
|
### Changed
|
|
17
17
|
- `[Both]` Security improvements and hardening. No action required.
|
|
18
18
|
|
|
19
|
+
### Fixed
|
|
20
|
+
- `[CLI]` The daemon no longer shows projects from other accounts after switching logins on the same machine. Project registrations are now tied to the account that created them, and the daemon cross-checks the server's per-account project list on startup. Projects from before this fix are matched to your account automatically on the next daemon start.
|
|
21
|
+
|
|
19
22
|
## [0.22.0] — 2026-05-23
|
|
20
23
|
|
|
21
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -123,6 +123,7 @@ agentdesk team <TASK-ID> Run a team session on an existing task
|
|
|
123
123
|
agentdesk team -d "..." Describe what you want — task created automatically
|
|
124
124
|
agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
|
|
125
125
|
agentdesk daemon Start daemon for remote sessions
|
|
126
|
+
agentdesk security-check [-d <dir>] Multi-surface security audit (server / CLI / UI / prompts)
|
|
126
127
|
agentdesk update Update to the latest version
|
|
127
128
|
```
|
|
128
129
|
|
package/bin/agentdesk.mjs
CHANGED
|
@@ -74,6 +74,7 @@ if (!command || command === "help" || command === "--help") {
|
|
|
74
74
|
agentdesk team -d "..." Create a task and run a session
|
|
75
75
|
agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
|
|
76
76
|
agentdesk daemon Start daemon for remote sessions
|
|
77
|
+
agentdesk security-check [-d <dir>] Multi-surface security audit (server / CLI / UI / prompts)
|
|
77
78
|
agentdesk update Update to the latest version
|
|
78
79
|
|
|
79
80
|
Options:
|
|
@@ -207,6 +208,13 @@ else if (command === "daemon") {
|
|
|
207
208
|
await runDaemon();
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
else if (command === "security-check") {
|
|
212
|
+
const { runSecurityCheck, parseSecurityCheckArgs } = await import("../cli/security-check.mjs");
|
|
213
|
+
const opts = parseSecurityCheckArgs(args.slice(1));
|
|
214
|
+
const code = await runSecurityCheck(opts);
|
|
215
|
+
process.exit(code);
|
|
216
|
+
}
|
|
217
|
+
|
|
210
218
|
else if (command === "update") {
|
|
211
219
|
const { execSync } = await import("child_process");
|
|
212
220
|
const currentVersion = pkg.version;
|
package/cli/daemon.mjs
CHANGED
|
@@ -8,12 +8,12 @@ import { randomUUID } from "crypto";
|
|
|
8
8
|
import WebSocket from "ws";
|
|
9
9
|
import { detectProject } from "./detect.mjs";
|
|
10
10
|
import { loadConfig } from "./config.mjs";
|
|
11
|
-
import { getStoredApiKey } from "./login.mjs";
|
|
11
|
+
import { getStoredApiKey, ensureAccountIdentity } from "./login.mjs";
|
|
12
12
|
import { resolveTeam, generateTeamPrompt } from "./agents.mjs";
|
|
13
13
|
import { buildPrompt } from "./prompt.mjs";
|
|
14
14
|
import { createStreamParser } from "./stream-parser.mjs";
|
|
15
15
|
import { runOrchestrator, runPhasedOrchestrator } from "./orchestrator.mjs";
|
|
16
|
-
import { getRegisteredProjects, registerLocalProject } from "./projects.mjs";
|
|
16
|
+
import { getRegisteredProjects, registerLocalProject, claimLocalProjects } from "./projects.mjs";
|
|
17
17
|
import { buildTrackerUrl } from "./tracker-url.mjs";
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
import { dirname } from "path";
|
|
@@ -150,46 +150,100 @@ export async function runDaemon() {
|
|
|
150
150
|
process.exit(1);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
// 2. Load registered projects
|
|
153
|
+
// 2. Load registered projects, scoped to the logged-in account (AD-64).
|
|
154
|
+
// The local registry is machine-global — it accumulates projects from
|
|
155
|
+
// every account ever used on this machine — so the server's per-account
|
|
156
|
+
// project list is the authority on what this account may see.
|
|
154
157
|
const agentdeskServer = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
155
|
-
|
|
158
|
+
const creds = await ensureAccountIdentity();
|
|
159
|
+
const accountId = creds?.accountId || null;
|
|
156
160
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
let serverIds = null;
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch(`${agentdeskServer}/api/projects`, {
|
|
164
|
+
headers: { "x-api-key": apiKey },
|
|
165
|
+
signal: AbortSignal.timeout(5000),
|
|
166
|
+
});
|
|
167
|
+
if (res.ok) {
|
|
168
|
+
const serverProjects = await res.json();
|
|
169
|
+
if (Array.isArray(serverProjects)) {
|
|
170
|
+
serverIds = new Set(serverProjects.map(sp => sp.id || sp.name));
|
|
171
|
+
|
|
172
|
+
// Untagged local entries with no server row — either they predate
|
|
173
|
+
// server-side registration or were initialized under another login.
|
|
174
|
+
// Offer them to the server: it accepts free ids and silently no-ops
|
|
175
|
+
// ids owned by another account (AD-23), so re-fetching the list
|
|
176
|
+
// tells us which ones are actually ours.
|
|
177
|
+
const unclaimed = getRegisteredProjects().filter(p =>
|
|
178
|
+
!p.accountId && !serverIds.has(p.id) &&
|
|
179
|
+
existsSync(p.path) && existsSync(join(p.path, ".agentdesk.json")));
|
|
180
|
+
if (unclaimed.length > 0) {
|
|
181
|
+
for (const p of unclaimed) {
|
|
182
|
+
try {
|
|
183
|
+
await fetch(`${agentdeskServer}/api/projects`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
186
|
+
body: JSON.stringify({ id: p.id, name: p.name, path: p.path }),
|
|
187
|
+
signal: AbortSignal.timeout(5000),
|
|
188
|
+
});
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const recheck = await fetch(`${agentdeskServer}/api/projects`, {
|
|
193
|
+
headers: { "x-api-key": apiKey },
|
|
194
|
+
signal: AbortSignal.timeout(5000),
|
|
195
|
+
});
|
|
196
|
+
if (recheck.ok) {
|
|
197
|
+
const confirmed = await recheck.json();
|
|
198
|
+
if (Array.isArray(confirmed)) {
|
|
199
|
+
serverIds = new Set(confirmed.map(sp => sp.id || sp.name));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {}
|
|
203
|
+
for (const p of unclaimed) {
|
|
204
|
+
if (!serverIds.has(p.id)) {
|
|
205
|
+
console.log(` ${yellow}Skipping ${p.name}${reset} ${dim}— registered to a different AgentDesk account${reset}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Claim legacy untagged local entries the server confirmed as ours
|
|
211
|
+
claimLocalProjects([...serverIds], accountId);
|
|
212
|
+
|
|
213
|
+
// Sync server projects missing from the local registry (pre-0.7.0
|
|
214
|
+
// setups, or projects initialized on another machine)
|
|
215
|
+
const localIds = new Set(getRegisteredProjects().map(p => p.id));
|
|
216
|
+
const missing = serverProjects.filter(sp => !localIds.has(sp.id || sp.name));
|
|
217
|
+
if (missing.length > 0) {
|
|
218
|
+
console.log(` ${dim}Syncing ${missing.length} project(s) from server...${reset}`);
|
|
219
|
+
for (const sp of missing) {
|
|
169
220
|
const projectName = sp.id || sp.name;
|
|
170
221
|
// If server has a valid path, use it
|
|
171
222
|
if (sp.path && existsSync(sp.path)) {
|
|
172
|
-
registerLocalProject(projectName, sp.name, sp.path);
|
|
223
|
+
registerLocalProject(projectName, sp.name, sp.path, accountId);
|
|
173
224
|
continue;
|
|
174
225
|
}
|
|
175
226
|
// Server has empty path — try to find the project locally
|
|
176
227
|
const found = findProjectLocally(projectName);
|
|
177
228
|
if (found) {
|
|
178
229
|
console.log(` ${dim}Found ${projectName} at ${found}${reset}`);
|
|
179
|
-
registerLocalProject(projectName, sp.name, found);
|
|
230
|
+
registerLocalProject(projectName, sp.name, found, accountId);
|
|
180
231
|
} else {
|
|
181
232
|
console.log(` ${yellow}Could not find ${projectName} locally.${reset} Run ${cyan}agentdesk init${reset} in its directory.`);
|
|
182
233
|
}
|
|
183
234
|
}
|
|
184
|
-
allProjects = getRegisteredProjects();
|
|
185
235
|
}
|
|
186
236
|
}
|
|
187
|
-
} catch {
|
|
188
|
-
// Server not reachable — continue with local only
|
|
189
237
|
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Server not reachable — fall back to account-tagged local entries
|
|
190
240
|
}
|
|
191
241
|
|
|
192
|
-
const projects =
|
|
242
|
+
const projects = getRegisteredProjects(accountId).filter(p => {
|
|
243
|
+
// Server reachable → it is authoritative: hide entries it doesn't own,
|
|
244
|
+
// even legacy untagged ones (they may belong to another account).
|
|
245
|
+
// Offline → tagged + untagged entries from getRegisteredProjects().
|
|
246
|
+
if (serverIds && !serverIds.has(p.id) && !(accountId && p.accountId === accountId)) return false;
|
|
193
247
|
if (!existsSync(p.path)) return false;
|
|
194
248
|
if (!existsSync(join(p.path, ".agentdesk.json"))) return false;
|
|
195
249
|
return true;
|
package/cli/init.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
21
21
|
import { join } from "path";
|
|
22
22
|
import { loadConfig, pushConfig } from "./config.mjs";
|
|
23
|
-
import { getStoredApiKey } from "./login.mjs";
|
|
23
|
+
import { getStoredApiKey, ensureAccountIdentity } from "./login.mjs";
|
|
24
24
|
import { registerLocalProject } from "./projects.mjs";
|
|
25
25
|
import { checkTrackerPermissions } from "./tracker-check.mjs";
|
|
26
26
|
import { autoMatchProject } from "./bootstrap.mjs";
|
|
@@ -535,7 +535,10 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
|
|
538
|
+
// AD-64: tag the registry entry with the account so the daemon can scope
|
|
539
|
+
// its project list per login.
|
|
540
|
+
const accountId = (await ensureAccountIdentity())?.accountId || null;
|
|
541
|
+
registerLocalProject(computedKey, projectName, currentCwd, accountId);
|
|
539
542
|
|
|
540
543
|
console.log("");
|
|
541
544
|
const cmdPrefix = trackerTeamOrProjectId || computedKey.toUpperCase();
|
package/cli/login.mjs
CHANGED
|
@@ -17,13 +17,42 @@ function getEnvApiKey() {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function getStoredApiKey() {
|
|
20
|
+
return getStoredCredentials()?.apiKey || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getStoredCredentials() {
|
|
20
24
|
if (!existsSync(CREDENTIALS_PATH)) return null;
|
|
21
25
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
return JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
27
|
+
} catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// AD-64: resolve which account an API key belongs to, so local project
|
|
31
|
+
// registry entries can be scoped per account.
|
|
32
|
+
async function fetchAccountIdentity(apiKey) {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${AGENTDESK_SERVER}/api/me`, {
|
|
35
|
+
headers: { "x-api-key": apiKey },
|
|
36
|
+
signal: AbortSignal.timeout(5000),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) return null;
|
|
39
|
+
const me = await res.json();
|
|
40
|
+
return me?.id ? { accountId: me.id, email: me.email || null } : null;
|
|
24
41
|
} catch { return null; }
|
|
25
42
|
}
|
|
26
43
|
|
|
44
|
+
// Backfill accountId into credentials saved by pre-AD-64 versions.
|
|
45
|
+
// Returns the (possibly updated) credentials object.
|
|
46
|
+
export async function ensureAccountIdentity() {
|
|
47
|
+
const creds = getStoredCredentials();
|
|
48
|
+
if (!creds?.apiKey || creds.accountId) return creds;
|
|
49
|
+
const identity = await fetchAccountIdentity(creds.apiKey);
|
|
50
|
+
if (!identity) return creds;
|
|
51
|
+
const updated = { ...creds, ...identity };
|
|
52
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(updated, null, 2), { mode: 0o600 });
|
|
53
|
+
return updated;
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
export async function runLogin() {
|
|
28
57
|
console.log("");
|
|
29
58
|
console.log(" AgentDesk — Login");
|
|
@@ -120,7 +149,16 @@ export async function runLogin() {
|
|
|
120
149
|
console.log(" agentdesk team TASK-123");
|
|
121
150
|
console.log("");
|
|
122
151
|
|
|
123
|
-
|
|
152
|
+
// AD-64: attach the account identity so the project registry can be
|
|
153
|
+
// scoped per account. Best-effort — login still succeeds without it.
|
|
154
|
+
fetchAccountIdentity(apiKey)
|
|
155
|
+
.then(identity => {
|
|
156
|
+
if (identity) {
|
|
157
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey, name, ...identity, savedAt: Date.now() }, null, 2), { mode: 0o600 });
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.catch(() => {})
|
|
161
|
+
.finally(() => { server.close(); process.exit(0); });
|
|
124
162
|
} else {
|
|
125
163
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
126
164
|
res.end("<html><body><h2>Login failed. No API key received.</h2></body></html>");
|
package/cli/projects.mjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
// Local project registry — tracks which projects have been initialized with `agentdesk init`
|
|
2
|
+
//
|
|
3
|
+
// AD-64: entries are tagged with the accountId they were registered under so
|
|
4
|
+
// the daemon can scope its project list to the logged-in account. Entries
|
|
5
|
+
// written by older versions have no accountId ("untagged") — the daemon
|
|
6
|
+
// claims them for the active account when the server confirms ownership.
|
|
2
7
|
|
|
3
8
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
4
9
|
import { join } from "path";
|
|
@@ -6,7 +11,7 @@ import { join } from "path";
|
|
|
6
11
|
const CONFIG_DIR = join(process.env.HOME || process.env.USERPROFILE, ".agentdesk");
|
|
7
12
|
const PROJECTS_PATH = join(CONFIG_DIR, "projects.json");
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
function readRegistry() {
|
|
10
15
|
try {
|
|
11
16
|
if (!existsSync(PROJECTS_PATH)) return [];
|
|
12
17
|
const data = JSON.parse(readFileSync(PROJECTS_PATH, "utf-8"));
|
|
@@ -16,10 +21,28 @@ export function getRegisteredProjects() {
|
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
function writeRegistry(projects) {
|
|
25
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
26
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
27
|
+
}
|
|
28
|
+
writeFileSync(PROJECTS_PATH, JSON.stringify({ projects }, null, 2) + "\n", { mode: 0o600 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// accountId: return only that account's entries plus legacy untagged ones
|
|
32
|
+
// (callers that can reach the server should claimLocalProjects() first so
|
|
33
|
+
// untagged entries get resolved rather than leaking across accounts).
|
|
34
|
+
export function getRegisteredProjects(accountId) {
|
|
35
|
+
const projects = readRegistry();
|
|
36
|
+
if (!accountId) return projects;
|
|
37
|
+
return projects.filter(p => !p.accountId || p.accountId === accountId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerLocalProject(id, name, path, accountId) {
|
|
41
|
+
const projects = readRegistry();
|
|
21
42
|
const existing = projects.findIndex(p => p.id === id);
|
|
22
43
|
const entry = { id, name, path, registeredAt: Date.now() };
|
|
44
|
+
if (accountId) entry.accountId = accountId;
|
|
45
|
+
else if (existing >= 0 && projects[existing].accountId) entry.accountId = projects[existing].accountId;
|
|
23
46
|
|
|
24
47
|
if (existing >= 0) {
|
|
25
48
|
projects[existing] = entry;
|
|
@@ -27,8 +50,20 @@ export function registerLocalProject(id, name, path) {
|
|
|
27
50
|
projects.push(entry);
|
|
28
51
|
}
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
writeRegistry(projects);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Tag untagged entries whose ids the server confirmed belong to accountId.
|
|
57
|
+
export function claimLocalProjects(ids, accountId) {
|
|
58
|
+
if (!accountId || !ids?.length) return;
|
|
59
|
+
const idSet = new Set(ids);
|
|
60
|
+
const projects = readRegistry();
|
|
61
|
+
let changed = false;
|
|
62
|
+
for (const p of projects) {
|
|
63
|
+
if (!p.accountId && idSet.has(p.id)) {
|
|
64
|
+
p.accountId = accountId;
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
32
67
|
}
|
|
33
|
-
|
|
68
|
+
if (changed) writeRegistry(projects);
|
|
34
69
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// `agentdesk security-check` — runs a multi-surface security audit on a
|
|
2
|
+
// codebase by spawning a Claude process scoped to the target directory.
|
|
3
|
+
//
|
|
4
|
+
// The audit pattern: identify which surfaces exist (server / CLI / UI /
|
|
5
|
+
// prompts), spawn parallel investigators per surface via Claude's Agent
|
|
6
|
+
// tool, return one triaged punch list (Critical / High / Medium / Low)
|
|
7
|
+
// with file:line citations and concrete fix recommendations.
|
|
8
|
+
//
|
|
9
|
+
// This is a batch full-codebase audit, NOT a PR/diff review. For diff
|
|
10
|
+
// review use Claude Code's built-in `/security-review` skill instead.
|
|
11
|
+
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
14
|
+
import { resolve, join } from "path";
|
|
15
|
+
|
|
16
|
+
const PROMPT = `You are running a multi-surface security audit on the codebase in the current working directory.
|
|
17
|
+
|
|
18
|
+
Goal: produce one triaged punch list of security findings (Critical / High / Medium / Low) with file:line citations and concrete fix recommendations.
|
|
19
|
+
|
|
20
|
+
Step 1 — Survey what's here:
|
|
21
|
+
- Server-shaped dirs: server/, api/, backend/, services/, plus *.py / *.go / *.rs / *.mjs / *.ts that look like HTTP/WS server entrypoints.
|
|
22
|
+
- CLI-shaped dirs: bin/, cli/, cmd/, top-level scripts.
|
|
23
|
+
- UI-shaped dirs: src/, app/, web/, frontend/, components/, index.html.
|
|
24
|
+
- Prompts / config inlined into LLM calls: prompts/, .claude/, CLAUDE.md, MCP configs, agent skill files.
|
|
25
|
+
|
|
26
|
+
Tell the user which surfaces you'll cover before launching investigators. Skip any that don't exist.
|
|
27
|
+
|
|
28
|
+
Step 2 — Spawn parallel investigators (one per existing surface) via the Agent tool with subagent_type "general-purpose". Send them in a single message with multiple tool calls so they run concurrently.
|
|
29
|
+
|
|
30
|
+
Each investigator gets the surface path and a focused checklist:
|
|
31
|
+
|
|
32
|
+
Server / API — auth bypass (missing auth gates, JWT weaknesses, CSRF, session fixation), authorization / IDOR on every :id route, input validation / injection (SQL via string concat, command injection, path traversal, SSRF, eval, prototype pollution), credential handling (encryption mode, IV reuse, key derivation, leak surfaces), rate limiting on auth endpoints, WebSocket auth (auth-by-message is non-standard), dependency vulns, secret exposure, error handling that leaks stack traces.
|
|
33
|
+
|
|
34
|
+
CLI / daemon — command injection (every exec/execSync template-literal sink), path traversal in fs.writeFile / path.join, sandbox claims vs implementation (denylists are usually wrong), credential exposure (tokens in logs/argv/errors/temp files), URL parsing SSRF, prompt-injection vectors when CLI feeds external content into an LLM, daemon-push abuse (signed? confirmation required?), config/credential precedence bugs.
|
|
35
|
+
|
|
36
|
+
UI / frontend / prompts — XSS in any dangerouslySetInnerHTML or markdown render of user-controlled content (agent output is untrusted), JWT/secrets in localStorage, bundled VITE_/NEXT_PUBLIC env vars in the public bundle, CSRF / SameSite, open redirect / clickjacking / missing frame-ancestors, <a href> without scheme allowlist (javascript: URLs run same-origin), prompt-injection robustness (untrusted-data delimiters? hard system rule against following directives in user content?), dependency vulns, CSP / security headers.
|
|
37
|
+
|
|
38
|
+
Each investigator returns a triaged list (Critical / High / Medium / Low) with file:line citations and concrete fix recommendations. Cap their responses at ~700 words; tell them to prioritize ruthlessly.
|
|
39
|
+
|
|
40
|
+
Step 3 — Synthesize:
|
|
41
|
+
- Group findings by severity (not by surface — the user wants the queue, not the org chart).
|
|
42
|
+
- Look for chains where a Medium reaches a Critical — note them.
|
|
43
|
+
- Identify the 2-3 structural patterns driving the findings (e.g., "untrusted data treated as trusted across boundaries", "sandbox claim doesn't match implementation"). These root causes are more useful than per-line fixes.
|
|
44
|
+
- Propose a triage plan in dependency order (which fixes ship together, what order minimizes the exposure window).
|
|
45
|
+
|
|
46
|
+
Step 4 — Present:
|
|
47
|
+
|
|
48
|
+
# Security audit — <project name> — <date>
|
|
49
|
+
|
|
50
|
+
## Headline numbers
|
|
51
|
+
- N Critical (exploitable as-is)
|
|
52
|
+
- N High (real risk, needs a chain or specific condition)
|
|
53
|
+
- N Medium (defense-in-depth gaps)
|
|
54
|
+
- N Low (hardening opportunities)
|
|
55
|
+
|
|
56
|
+
## The N things to fix this week
|
|
57
|
+
<top items with file:line + why-exploitable + concrete fix>
|
|
58
|
+
|
|
59
|
+
## Full punch list (compact)
|
|
60
|
+
### CRITICAL
|
|
61
|
+
- file.ext:LL — short description. Why exploitable: ... Suggested fix: ...
|
|
62
|
+
### HIGH ...
|
|
63
|
+
### MEDIUM ...
|
|
64
|
+
### LOW ...
|
|
65
|
+
|
|
66
|
+
## The pattern
|
|
67
|
+
<2-3 sentence root-cause analysis>
|
|
68
|
+
|
|
69
|
+
## Suggested triage plan
|
|
70
|
+
<ordered plan with rough day-by-day>
|
|
71
|
+
|
|
72
|
+
Important:
|
|
73
|
+
- Do NOT fix anything. Audit and report only.
|
|
74
|
+
- Be honest about scope — a real audit is days. This one pass produces a focused triage list.
|
|
75
|
+
- For any CHANGELOG entry referencing security work, use NEUTRAL phrasing ("Security improvements. No action required.") — never name the bug class on public release notes.
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
function colorize() {
|
|
79
|
+
const isTTY = process.stdout.isTTY;
|
|
80
|
+
const c = (code) => (s) => isTTY ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
81
|
+
return { dim: c(2), green: c(32), cyan: c(36), yellow: c(33), red: c(31) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function runSecurityCheck({ cwd, out }) {
|
|
85
|
+
const { dim, green, cyan, yellow, red } = colorize();
|
|
86
|
+
const targetDir = resolve(cwd || process.cwd());
|
|
87
|
+
if (!existsSync(targetDir)) {
|
|
88
|
+
console.error(`${red("Directory not found:")} ${targetDir}`);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default output path: .agentdesk/security-audit-<timestamp>.md inside the audited project.
|
|
93
|
+
const stamp = new Date().toISOString().replace(/[:T]/g, "-").slice(0, 16);
|
|
94
|
+
const outPath = resolve(out || join(targetDir, ".agentdesk", `security-audit-${stamp}.md`));
|
|
95
|
+
mkdirSync(join(outPath, ".."), { recursive: true });
|
|
96
|
+
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log(` ${green("agentdesk security-check")}`);
|
|
99
|
+
console.log(` ${dim("Target:")} ${targetDir}`);
|
|
100
|
+
console.log(` ${dim("Report:")} ${outPath}`);
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log(` ${dim("Spawning Claude with the audit prompt. This typically takes 5–15 minutes.")}`);
|
|
103
|
+
console.log(` ${dim("Output streams to your terminal AND lands in the report file when done.")}`);
|
|
104
|
+
console.log("");
|
|
105
|
+
|
|
106
|
+
// Spawn `claude -p <prompt>` against the target directory. The prompt tells
|
|
107
|
+
// Claude to use its Agent tool to fan out to parallel investigators.
|
|
108
|
+
const child = spawn("claude", ["-p", PROMPT, "--permission-mode", "acceptEdits"], {
|
|
109
|
+
cwd: targetDir,
|
|
110
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
111
|
+
env: process.env,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let captured = "";
|
|
115
|
+
child.stdout.on("data", (buf) => {
|
|
116
|
+
const chunk = buf.toString();
|
|
117
|
+
captured += chunk;
|
|
118
|
+
process.stdout.write(chunk);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return new Promise((resolveExit) => {
|
|
122
|
+
child.on("error", (err) => {
|
|
123
|
+
console.error(`\n ${red("Failed to spawn claude:")} ${err.message}`);
|
|
124
|
+
console.error(` ${dim("Is the Claude Code CLI installed and on your PATH? https://claude.ai/code")}`);
|
|
125
|
+
resolveExit(1);
|
|
126
|
+
});
|
|
127
|
+
child.on("close", (code) => {
|
|
128
|
+
try {
|
|
129
|
+
const header = [
|
|
130
|
+
`# Security audit — ${targetDir.split("/").pop()}`,
|
|
131
|
+
`Generated: ${new Date().toISOString()}`,
|
|
132
|
+
`Target: \`${targetDir}\``,
|
|
133
|
+
`Tool: agentdesk security-check`,
|
|
134
|
+
``,
|
|
135
|
+
`---`,
|
|
136
|
+
``,
|
|
137
|
+
].join("\n");
|
|
138
|
+
writeFileSync(outPath, header + captured);
|
|
139
|
+
console.log("");
|
|
140
|
+
console.log(` ${green("Report written:")} ${outPath}`);
|
|
141
|
+
if (code !== 0) {
|
|
142
|
+
console.log(` ${yellow("Claude exited with code")} ${code}${dim(" — report may be partial.")}`);
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`\n ${red("Failed to write report:")} ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
resolveExit(code === null ? 1 : code);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function parseSecurityCheckArgs(args) {
|
|
153
|
+
let cwd = null;
|
|
154
|
+
let out = null;
|
|
155
|
+
for (let i = 0; i < args.length; i++) {
|
|
156
|
+
if ((args[i] === "--cwd" || args[i] === "-d") && args[i + 1]) {
|
|
157
|
+
cwd = args[++i];
|
|
158
|
+
} else if (args[i] === "--out" && args[i + 1]) {
|
|
159
|
+
out = args[++i];
|
|
160
|
+
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
161
|
+
printHelp();
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { cwd, out };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function printHelp() {
|
|
169
|
+
console.log(`
|
|
170
|
+
agentdesk security-check — multi-surface security audit
|
|
171
|
+
|
|
172
|
+
Usage:
|
|
173
|
+
agentdesk security-check Audit the current directory
|
|
174
|
+
agentdesk security-check -d <dir> Audit a different directory
|
|
175
|
+
agentdesk security-check --out <file> Custom report output path
|
|
176
|
+
|
|
177
|
+
Spawns a Claude process scoped to the target directory. Claude surveys
|
|
178
|
+
the codebase, fans out to parallel investigators (server / CLI / UI /
|
|
179
|
+
prompts), and returns a triaged punch list with file:line citations.
|
|
180
|
+
|
|
181
|
+
Output:
|
|
182
|
+
Streams to your terminal as Claude works, and saves a copy to:
|
|
183
|
+
<target>/.agentdesk/security-audit-<timestamp>.md (by default)
|
|
184
|
+
|
|
185
|
+
Requires the Claude Code CLI on your PATH. https://claude.ai/code
|
|
186
|
+
`);
|
|
187
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kendoo.agentdesk/agentdesk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"server": "node server/index.mjs",
|
|
23
23
|
"build": "vite build",
|
|
24
24
|
"preview": "vite preview",
|
|
25
|
-
"test": "node --test tests/server.test.mjs tests/agents.test.mjs tests/homepage.test.mjs tests/sessionUtils.test.mjs tests/tracker-url.test.mjs tests/session-preflight.test.mjs tests/access.test.mjs tests/project-ownership.test.mjs tests/random-hex.test.mjs",
|
|
25
|
+
"test": "node --test tests/server.test.mjs tests/agents.test.mjs tests/homepage.test.mjs tests/sessionUtils.test.mjs tests/tracker-url.test.mjs tests/session-preflight.test.mjs tests/access.test.mjs tests/project-ownership.test.mjs tests/random-hex.test.mjs tests/projects-registry.test.mjs",
|
|
26
26
|
"lint:changelog": "node scripts/lint-changelog.mjs",
|
|
27
27
|
"prepublishOnly": "node scripts/lint-changelog.mjs"
|
|
28
28
|
},
|