@coworker-jp/aidr 0.1.10 → 0.1.18
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/README.md +66 -14
- package/package.json +1 -1
- package/src/agents/standalone.mjs +3 -3
- package/src/binary-fetcher.mjs +6 -58
- package/src/cli.mjs +198 -151
- package/src/env-gate.mjs +208 -0
- package/src/red-team.mjs +169 -0
- package/src/scheduled.mjs +83 -222
- package/src/sentinel.mjs +143 -266
- package/src/templates.mjs +7 -6
- package/src/verify.mjs +3 -4
- package/src/browser-extension.mjs +0 -83
- package/src/invoker-resolve.mjs +0 -63
package/src/cli.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { AGENT_ORDER, AGENT_REGISTRY, loadAgent } from "./agents/index.mjs";
|
|
|
12
12
|
import { verifyKey, maskKey } from "./verify.mjs";
|
|
13
13
|
import { fetchBinary, fetchOpengrep } from "./binary-fetcher.mjs";
|
|
14
14
|
import { detectInstalled } from "./detect.mjs";
|
|
15
|
-
import {
|
|
15
|
+
import { evaluateInstallGate, formatLocalPcGuidance } from "./env-gate.mjs";
|
|
16
16
|
import { resolveCallerHome, isRunningUnderSudo, chownToCaller } from "./sudo-user.mjs";
|
|
17
17
|
|
|
18
18
|
// Package identity — read at module load from the bundled package.json.
|
|
@@ -84,47 +84,37 @@ async function cmdInstall(opts) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Install-environment gate (issue #243): an interactive terminal with zero
|
|
88
|
+
// cloud/CI/remote indicators is almost certainly someone's own PC — the
|
|
89
|
+
// machine that should run coworker-sentinel, not the ai-scanner hooks.
|
|
90
|
+
// Stop and point at the sentinel installer unless --local overrides.
|
|
91
|
+
// Non-interactive runs (CI, cloud sandboxes, agent-executed shells) pass
|
|
92
|
+
// silently: they are ai-scanner's intended environments.
|
|
93
|
+
if (!opts.local) {
|
|
94
|
+
const gate = evaluateInstallGate();
|
|
95
|
+
if (gate.block) {
|
|
96
|
+
console.error(formatLocalPcGuidance({
|
|
97
|
+
sentinelPlatform: gate.sentinelPlatform,
|
|
98
|
+
env: opts.env || DEFAULT_ENV,
|
|
99
|
+
}));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
87
104
|
const home = resolveCallerHome();
|
|
88
105
|
const scope = opts.scope || "user";
|
|
89
106
|
const base = scope === "user" ? home : process.cwd();
|
|
90
107
|
validateKey(opts.key);
|
|
91
108
|
|
|
92
|
-
let verifyInfo = null;
|
|
93
109
|
if (opts.verify !== false) {
|
|
94
110
|
try {
|
|
95
|
-
|
|
111
|
+
await verifyKey(opts.key, opts.env || DEFAULT_ENV);
|
|
96
112
|
} catch (e) {
|
|
97
113
|
console.error(`verify failed: ${e.message}`);
|
|
98
114
|
process.exit(1);
|
|
99
115
|
}
|
|
100
116
|
}
|
|
101
117
|
|
|
102
|
-
// --with-sentinel requires pro or trial plan
|
|
103
|
-
if (opts.withSentinel && verifyInfo && !verifyInfo.edr_enabled) {
|
|
104
|
-
console.error("Error: --with-sentinel requires a Pro or Trial plan.");
|
|
105
|
-
console.error(`Current plan: ${verifyInfo.plan}`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// --with-sentinel requires root (same as --scheduled)
|
|
110
|
-
if (opts.withSentinel && !opts.dryRun
|
|
111
|
-
&& process.getuid && process.getuid() !== 0
|
|
112
|
-
&& process.env.AI_SCANNER_FAKE_ROOT !== "1") {
|
|
113
|
-
const interactive = process.stdin.isTTY && process.stdout.isTTY && process.platform !== "win32";
|
|
114
|
-
if (interactive) {
|
|
115
|
-
console.error("--with-sentinel requires root. Re-running via sudo (you may be prompted for your password)...");
|
|
116
|
-
const result = spawnSync(
|
|
117
|
-
"sudo",
|
|
118
|
-
["-E", process.execPath, ...process.argv.slice(1)],
|
|
119
|
-
{ stdio: "inherit" }
|
|
120
|
-
);
|
|
121
|
-
process.exit(result.status ?? 1);
|
|
122
|
-
}
|
|
123
|
-
console.error("Error: --with-sentinel requires root privileges.");
|
|
124
|
-
console.error(` sudo -E npx @coworker-jp/aidr install --with-sentinel --key <ak_xxx>`);
|
|
125
|
-
process.exit(2);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
118
|
let agents;
|
|
129
119
|
if (opts.all) {
|
|
130
120
|
agents = detectInstalled(home);
|
|
@@ -146,53 +136,6 @@ async function cmdInstall(opts) {
|
|
|
146
136
|
}
|
|
147
137
|
}
|
|
148
138
|
|
|
149
|
-
// Standalone has no hook scripts and no settings.json — scheduled
|
|
150
|
-
// endpoint-info collection is the whole reason to pick it, so turn
|
|
151
|
-
// --scheduled on by default when standalone is selected. --binary-only
|
|
152
|
-
// still short-circuits later.
|
|
153
|
-
if (agents.includes("standalone")) {
|
|
154
|
-
opts.scheduled = true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// --scheduled installs a system-wide cron / systemd / launchd entry that
|
|
158
|
-
// runs `ai-scanner scan endpoint-info` periodically as root, against an
|
|
159
|
-
// agent-independent binary at /opt/coworker/aidr/bin/ai-scanner. Auto-add
|
|
160
|
-
// the standalone install so the binary fetch loop drops a copy there
|
|
161
|
-
// even when the user only listed AI agents (claude / cursor / ...) on
|
|
162
|
-
// the command line.
|
|
163
|
-
if (opts.scheduled && !agents.includes("standalone")) {
|
|
164
|
-
agents.push("standalone");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const intervalHours = parseInt(opts.interval ?? "4", 10);
|
|
168
|
-
if (opts.scheduled && (isNaN(intervalHours) || intervalHours < 1)) {
|
|
169
|
-
console.error("--interval must be an integer >= 1");
|
|
170
|
-
process.exit(1);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// --scheduled の system path 書き込みは root 必須。
|
|
174
|
-
// - TTY あり (対話 shell): `sudo -E` で自分自身を re-exec、password prompt が出る
|
|
175
|
-
// - TTY なし (CI / pipe): sudo は対話的に password を聞けないので fail-fast 案内
|
|
176
|
-
// smoke / unit test では `AI_SCANNER_FAKE_ROOT=1` を設定してこのチェックを skip 可能。
|
|
177
|
-
if (opts.scheduled && !opts.dryRun
|
|
178
|
-
&& process.getuid && process.getuid() !== 0
|
|
179
|
-
&& process.env.AI_SCANNER_FAKE_ROOT !== "1") {
|
|
180
|
-
const interactive = process.stdin.isTTY && process.stdout.isTTY && process.platform !== "win32";
|
|
181
|
-
if (interactive) {
|
|
182
|
-
console.error("--scheduled requires root. Re-running via sudo (you may be prompted for your password)...");
|
|
183
|
-
const result = spawnSync(
|
|
184
|
-
"sudo",
|
|
185
|
-
["-E", process.execPath, ...process.argv.slice(1)],
|
|
186
|
-
{ stdio: "inherit" }
|
|
187
|
-
);
|
|
188
|
-
process.exit(result.status ?? 1);
|
|
189
|
-
}
|
|
190
|
-
console.error("Error: --scheduled requires root privileges (writes /opt/coworker/aidr, /etc/systemd/system, /etc/aidr).");
|
|
191
|
-
console.error("Re-run with sudo (non-interactive shells cannot prompt for password):");
|
|
192
|
-
console.error(` sudo -E npx @coworker-jp/aidr install --scheduled --key <ak_xxx>`);
|
|
193
|
-
process.exit(2);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
139
|
// Confirmation for existing shared settings.json (claude / gemini).
|
|
197
140
|
// --yes bypasses; --dry-run doesn't write so skip; otherwise prompt on TTY,
|
|
198
141
|
// error if non-interactive (safer than silently merging in CI).
|
|
@@ -222,16 +165,9 @@ async function cmdInstall(opts) {
|
|
|
222
165
|
}
|
|
223
166
|
|
|
224
167
|
if (!opts.skipBinary && !opts.dryRun) {
|
|
225
|
-
const { SYSTEM_BIN_DIR } = await import("./scheduled.mjs");
|
|
226
168
|
const agentBinDirs = [];
|
|
227
169
|
for (const name of agents) {
|
|
228
170
|
const mod = await loadAgent(name);
|
|
229
|
-
// --scheduled 時の standalone は system path (/opt/coworker/aidr/bin)
|
|
230
|
-
// に置く。それ以外は従来通り per-user の ~/.<agent>/bin。
|
|
231
|
-
if (name === "standalone" && opts.scheduled) {
|
|
232
|
-
agentBinDirs.push(SYSTEM_BIN_DIR);
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
171
|
const agentDir = mod.meta.agentDir;
|
|
236
172
|
if (!agentDir) continue;
|
|
237
173
|
agentBinDirs.push(path.join(home, agentDir, "bin"));
|
|
@@ -263,10 +199,27 @@ async function cmdInstall(opts) {
|
|
|
263
199
|
const src = path.join(primaryDir, file);
|
|
264
200
|
const dest = path.join(restDir, file);
|
|
265
201
|
await fs.mkdir(restDir, { recursive: true });
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
202
|
+
// Temp + atomic rename, NEVER copyFile onto the live path — same
|
|
203
|
+
// rule (and same reason) as binary-fetcher.mjs::fetchAsset. On a
|
|
204
|
+
// re-install this path already holds a binary the user's agent has
|
|
205
|
+
// exec'd, and on macOS rewriting an exec'd file in place makes the
|
|
206
|
+
// kernel mark THAT VNODE's code signature invalid: every later exec
|
|
207
|
+
// of the inode is SIGKILLed until reboot, even though the bytes on
|
|
208
|
+
// disk are valid and re-running the installer cannot repair it
|
|
209
|
+
// (the inode never changes). See issue #171 / ADR-0005 §52.
|
|
210
|
+
// The temp file is in the destination dir, so the rename is on one
|
|
211
|
+
// filesystem = atomic, and mode/xattr are set BEFORE it goes live.
|
|
212
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
213
|
+
try {
|
|
214
|
+
await fs.copyFile(src, tmp);
|
|
215
|
+
await fs.chmod(tmp, 0o755);
|
|
216
|
+
if (process.platform === "darwin") {
|
|
217
|
+
try { await execFileP("xattr", ["-d", "com.apple.quarantine", tmp]); } catch {}
|
|
218
|
+
}
|
|
219
|
+
await fs.rename(tmp, dest);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
await fs.rm(tmp, { force: true }).catch(() => {});
|
|
222
|
+
throw e;
|
|
270
223
|
}
|
|
271
224
|
console.log(`binary: ${dest} (copied)`);
|
|
272
225
|
})
|
|
@@ -306,54 +259,29 @@ async function cmdInstall(opts) {
|
|
|
306
259
|
.filter((r) => r.name !== "standalone")
|
|
307
260
|
.map((r) => r.displayName);
|
|
308
261
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const presignedUrl = verifyInfo?.sentinel_download_urls?.[platform] || undefined;
|
|
330
|
-
// Windows additionally needs the sibling WinDivert.dll presigned URL.
|
|
331
|
-
const dllPresignedUrl =
|
|
332
|
-
verifyInfo?.sentinel_download_urls?.[`${platform}-windivert-dll`] || undefined;
|
|
333
|
-
await installSentinel({
|
|
334
|
-
accessKey: opts.key,
|
|
335
|
-
dryRun: opts.dryRun,
|
|
336
|
-
skipBinary: opts.skipBinary,
|
|
337
|
-
presignedUrl,
|
|
338
|
-
dllPresignedUrl,
|
|
339
|
-
env: opts.env || DEFAULT_ENV,
|
|
340
|
-
noActivate: process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE === "1",
|
|
341
|
-
});
|
|
342
|
-
} catch (e) {
|
|
343
|
-
console.error(`sentinel install failed: ${e.message}`);
|
|
344
|
-
process.exit(1);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (opts.browserExtension && !opts.dryRun) {
|
|
349
|
-
const firstAgentName = agents[0];
|
|
350
|
-
const firstAgent = await loadAgent(firstAgentName);
|
|
351
|
-
const binaryPath = path.join(home, firstAgent.meta.agentDir, "bin", "ai-scanner");
|
|
352
|
-
try {
|
|
353
|
-
await installBrowserExtension(home, binaryPath);
|
|
354
|
-
} catch (e) {
|
|
355
|
-
console.error(`browser extension install warning: ${e.message}`);
|
|
262
|
+
// Legacy `--scheduled` artifacts (cron.d / launchd plist / wrapper /
|
|
263
|
+
// scheduled.log) are retired — endpoint-info collection now runs from the
|
|
264
|
+
// coworker-sentinel daemon's internal timer. Remove leftovers from older
|
|
265
|
+
// installs when we have root; otherwise just point the user at sudo.
|
|
266
|
+
// /etc/aidr/aidr.env is intentionally preserved (sentinel reads it).
|
|
267
|
+
try {
|
|
268
|
+
const { isScheduledInstalled, cleanupLegacyScheduled } = await import("./scheduled.mjs");
|
|
269
|
+
if (await isScheduledInstalled()) {
|
|
270
|
+
const isRoot = process.env.AI_SCANNER_FAKE_ROOT === "1"
|
|
271
|
+
|| (process.getuid && process.getuid() === 0);
|
|
272
|
+
if (opts.dryRun) {
|
|
273
|
+
console.error("[dry-run] legacy scheduled endpoint-info entries detected; a real install would remove them (root required).");
|
|
274
|
+
} else if (isRoot) {
|
|
275
|
+
await cleanupLegacyScheduled();
|
|
276
|
+
console.error("Removed legacy scheduled endpoint-info entries (cron/launchd) — collection is now handled by the coworker-sentinel daemon.");
|
|
277
|
+
} else {
|
|
278
|
+
console.error("Note: legacy scheduled endpoint-info entries detected (cron/launchd). They are obsolete —");
|
|
279
|
+
console.error("collection is now handled by the coworker-sentinel daemon. Re-run with sudo to remove them:");
|
|
280
|
+
console.error(" sudo -E npx @coworker-jp/aidr install ...");
|
|
281
|
+
}
|
|
356
282
|
}
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.error(`legacy scheduled cleanup warning: ${e.message}`);
|
|
357
285
|
}
|
|
358
286
|
|
|
359
287
|
// Auto-sudo re-exec wrote per-agent files as root into the original user's
|
|
@@ -496,18 +424,20 @@ async function cmdUninstall(opts) {
|
|
|
496
424
|
}
|
|
497
425
|
}));
|
|
498
426
|
|
|
499
|
-
//
|
|
500
|
-
// and need root to remove. Mirror cmdInstall's
|
|
501
|
-
// re-exec、non-TTY は guidance を出して
|
|
427
|
+
// Legacy `--scheduled` entries live in system paths (/etc/cron.d,
|
|
428
|
+
// /Library/LaunchDaemons) and need root to remove. Mirror cmdInstall's
|
|
429
|
+
// auto-sudo: TTY なら sudo に re-exec、non-TTY は guidance を出して
|
|
430
|
+
// per-user 部分だけ撤去で完了。/etc/aidr/aidr.env は sentinel が読むので
|
|
431
|
+
// 残す (cleanupLegacyScheduled は触らない)。
|
|
502
432
|
try {
|
|
503
|
-
const {
|
|
433
|
+
const { cleanupLegacyScheduled, isScheduledInstalled } = await import("./scheduled.mjs");
|
|
504
434
|
if (await isScheduledInstalled()) {
|
|
505
435
|
const isRoot = process.env.AI_SCANNER_FAKE_ROOT === "1"
|
|
506
436
|
|| (process.getuid && process.getuid() === 0);
|
|
507
437
|
if (!isRoot) {
|
|
508
438
|
const interactive = process.stdin.isTTY && process.stdout.isTTY && process.platform !== "win32";
|
|
509
439
|
if (interactive) {
|
|
510
|
-
console.error("
|
|
440
|
+
console.error("Legacy scheduled entry detected. Re-running via sudo to remove (you may be prompted for your password)...");
|
|
511
441
|
const result = spawnSync(
|
|
512
442
|
"sudo",
|
|
513
443
|
["-E", process.execPath, ...process.argv.slice(1)],
|
|
@@ -515,19 +445,23 @@ async function cmdUninstall(opts) {
|
|
|
515
445
|
);
|
|
516
446
|
process.exit(result.status ?? 0);
|
|
517
447
|
}
|
|
518
|
-
console.error("Note: scheduled entry detected but uninstall needs root. Re-run:");
|
|
448
|
+
console.error("Note: legacy scheduled entry detected but uninstall needs root. Re-run:");
|
|
519
449
|
console.error(` sudo -E npx @coworker-jp/aidr uninstall`);
|
|
520
450
|
} else {
|
|
521
|
-
await
|
|
451
|
+
await cleanupLegacyScheduled({ dryRun: opts.dryRun });
|
|
522
452
|
}
|
|
523
453
|
}
|
|
524
454
|
} catch (e) {
|
|
525
|
-
console.error(`scheduled
|
|
455
|
+
console.error(`scheduled cleanup warning: ${e.message}`);
|
|
526
456
|
}
|
|
527
457
|
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
458
|
+
// LEGACY npx-installed sentinel daemon (Linux systemd unit under
|
|
459
|
+
// /etc/systemd/system + /opt binaries) — same auto-sudo pattern.
|
|
460
|
+
// isSentinelInstalled matches only that legacy layout: deb installs
|
|
461
|
+
// (unit under /lib/systemd/system), the Windows setup.exe, and the macOS
|
|
462
|
+
// DMG are owned by their own uninstallers and never touched here.
|
|
463
|
+
// Independent try-block so a scheduled-uninstall hiccup doesn't skip
|
|
464
|
+
// sentinel cleanup (and vice-versa).
|
|
531
465
|
try {
|
|
532
466
|
const { uninstallSentinel, isSentinelInstalled } = await import("./sentinel.mjs");
|
|
533
467
|
if (await isSentinelInstalled()) {
|
|
@@ -536,7 +470,7 @@ async function cmdUninstall(opts) {
|
|
|
536
470
|
if (!isRoot) {
|
|
537
471
|
const interactive = process.stdin.isTTY && process.stdout.isTTY && process.platform !== "win32";
|
|
538
472
|
if (interactive) {
|
|
539
|
-
console.error("
|
|
473
|
+
console.error("Legacy npx-installed sentinel daemon detected. Re-running via sudo to remove (you may be prompted for your password)...");
|
|
540
474
|
const result = spawnSync(
|
|
541
475
|
"sudo",
|
|
542
476
|
["-E", process.execPath, ...process.argv.slice(1)],
|
|
@@ -544,7 +478,7 @@ async function cmdUninstall(opts) {
|
|
|
544
478
|
);
|
|
545
479
|
process.exit(result.status ?? 0);
|
|
546
480
|
}
|
|
547
|
-
console.error("Note: sentinel daemon detected but uninstall needs root. Re-run:");
|
|
481
|
+
console.error("Note: legacy npx-installed sentinel daemon detected but uninstall needs root. Re-run:");
|
|
548
482
|
console.error(` sudo -E npx @coworker-jp/aidr uninstall`);
|
|
549
483
|
} else {
|
|
550
484
|
await uninstallSentinel({ dryRun: opts.dryRun });
|
|
@@ -588,7 +522,101 @@ async function cmdListAgents() {
|
|
|
588
522
|
}
|
|
589
523
|
}
|
|
590
524
|
|
|
525
|
+
// `--scheduled` / `--interval` were retired when endpoint-info collection
|
|
526
|
+
// moved into the coworker-sentinel daemon (internal 6h timer), and
|
|
527
|
+
// `--with-sentinel` was retired when sentinel distribution moved to the
|
|
528
|
+
// OS-native installers (setup.exe / DMG / deb). Commander no longer knows
|
|
529
|
+
// the flags, so scan argv ourselves to fail with a migration message
|
|
530
|
+
// instead of a generic "unknown option" error.
|
|
531
|
+
const SENTINEL_OS_INSTALLERS_HINT = `coworker-sentinel is installed with the OS-native installer (get it from the user portal's Download page):
|
|
532
|
+
Windows: coworker-sentinel-setup.exe
|
|
533
|
+
macOS: CoworkerSentinel.dmg (drag to Applications, launch, enter your access key)
|
|
534
|
+
Linux: sudo apt install ./coworker-sentinel.deb && sudo coworker-sentinel activate --key <ak_xxx>`;
|
|
535
|
+
|
|
536
|
+
const REMOVED_SCHEDULED_FLAGS_MESSAGE = `Error: --scheduled / --interval are no longer supported.
|
|
537
|
+
Endpoint-info collection is now scheduled by the coworker-sentinel daemon (every 6 hours).
|
|
538
|
+
${SENTINEL_OS_INSTALLERS_HINT}
|
|
539
|
+
Toggle collection from the macOS menu-bar app, or edit /etc/aidr/sentinel-settings.json
|
|
540
|
+
(Windows: %ProgramData%\\coworker\\sentinel-settings.json).`;
|
|
541
|
+
|
|
542
|
+
const REMOVED_WITH_SENTINEL_MESSAGE = `Error: --with-sentinel is no longer supported.
|
|
543
|
+
${SENTINEL_OS_INSTALLERS_HINT}
|
|
544
|
+
aidr now installs agent hooks only:
|
|
545
|
+
npx @coworker-jp/aidr install --agent <list> --key <ak_xxx>`;
|
|
546
|
+
|
|
547
|
+
// Red Team は hook 方式のエージェント統合とは別物 — hook を撒くのではなく、
|
|
548
|
+
// ネットワーク脆弱性スキャナを MCP サーバーとして 1 つ登録するだけ。そのため
|
|
549
|
+
// install/uninstall の --agent 複数指定・--all の枠組みには乗せず、独立した
|
|
550
|
+
// サブコマンドにしている。実装は red-team.mjs に分離 (scheduled/sentinel と
|
|
551
|
+
// 同じ遅延 import パターン)。
|
|
552
|
+
async function cmdRedTeamInstall(opts) {
|
|
553
|
+
const agent = opts.agent || "claude";
|
|
554
|
+
if (agent !== "claude") {
|
|
555
|
+
console.error(`red-team install: unsupported agent '${agent}' (only 'claude' is supported)`);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const home = resolveCallerHome();
|
|
560
|
+
const scope = opts.scope || "user";
|
|
561
|
+
validateKey(opts.key);
|
|
562
|
+
|
|
563
|
+
const env = opts.env || DEFAULT_ENV;
|
|
564
|
+
if (opts.verify !== false && !opts.dryRun) {
|
|
565
|
+
try {
|
|
566
|
+
await verifyKey(opts.key, env);
|
|
567
|
+
} catch (e) {
|
|
568
|
+
console.error(`verify failed: ${e.message}`);
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const { installRedTeam } = await import("./red-team.mjs");
|
|
574
|
+
try {
|
|
575
|
+
const res = await installRedTeam({
|
|
576
|
+
accessKey: opts.key,
|
|
577
|
+
env,
|
|
578
|
+
scope,
|
|
579
|
+
home: scope === "user" ? home : process.cwd(),
|
|
580
|
+
dryRun: opts.dryRun,
|
|
581
|
+
});
|
|
582
|
+
if (res.installed) {
|
|
583
|
+
console.log("");
|
|
584
|
+
console.log("Next: restart Claude Code so the MCP server is picked up.");
|
|
585
|
+
}
|
|
586
|
+
} catch (e) {
|
|
587
|
+
// 403 red_team_requires_10_seats は「Pro/Trial かつ契約シート 10 以上」の
|
|
588
|
+
// エンタイトルメント不足。サーバー側が唯一の判断者なので、ここでは
|
|
589
|
+
// メッセージをそのまま見せる。
|
|
590
|
+
console.error(`red-team install failed: ${e.message}`);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function cmdRedTeamUninstall(opts) {
|
|
596
|
+
const home = resolveCallerHome();
|
|
597
|
+
const scope = opts.scope || "user";
|
|
598
|
+
const { uninstallRedTeam } = await import("./red-team.mjs");
|
|
599
|
+
await uninstallRedTeam({
|
|
600
|
+
scope,
|
|
601
|
+
home: scope === "user" ? home : process.cwd(),
|
|
602
|
+
dryRun: opts.dryRun,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function rejectRemovedFlags(argv) {
|
|
607
|
+
const args = argv.slice(2);
|
|
608
|
+
if (args.some((a) => a === "--scheduled" || a === "--interval" || a.startsWith("--interval="))) {
|
|
609
|
+
console.error(REMOVED_SCHEDULED_FLAGS_MESSAGE);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
if (args.includes("--with-sentinel")) {
|
|
613
|
+
console.error(REMOVED_WITH_SENTINEL_MESSAGE);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
591
618
|
export async function run(argv) {
|
|
619
|
+
rejectRemovedFlags(argv);
|
|
592
620
|
const program = new Command();
|
|
593
621
|
program
|
|
594
622
|
.name("aidr")
|
|
@@ -608,12 +636,9 @@ export async function run(argv) {
|
|
|
608
636
|
.option("--no-verify", "skip /verify call")
|
|
609
637
|
.option("--skip-binary", "skip binary download", false)
|
|
610
638
|
.option("--scope <project|user>", "installation scope: user ($HOME) or project (cwd)", "user")
|
|
639
|
+
.option("--local", "install on this local PC anyway (ai-scanner targets cloud/CI agent environments; a personal PC should run coworker-sentinel)", false)
|
|
611
640
|
.option("-y, --yes", "proceed without confirmation when existing settings.json is detected", false)
|
|
612
641
|
.option("--uninstall", "remove all ai-scanner hooks/scripts/binaries for the selected agents (see 'uninstall' command)", false)
|
|
613
|
-
.option("--scheduled", "enable scheduled endpoint-info collection (creates a cron/systemd/launchd entry; without this flag endpoint-info is never collected)", false)
|
|
614
|
-
.option("--interval <hours>", "scheduled scan interval in hours (min 1, default 4; requires --scheduled)", "4")
|
|
615
|
-
.option("--with-sentinel", "install coworker-sentinel host-level EDR daemon (Pro/Trial plan; requires sudo)", false)
|
|
616
|
-
.option("--browser-extension", "install browser extension native messaging host for Shadow IT network monitoring (default: disabled)", false)
|
|
617
642
|
.action(cmdInstall);
|
|
618
643
|
|
|
619
644
|
program
|
|
@@ -626,6 +651,28 @@ export async function run(argv) {
|
|
|
626
651
|
.option("--dry-run", "print what would be removed without touching disk", false)
|
|
627
652
|
.action(cmdUninstall);
|
|
628
653
|
|
|
654
|
+
const redTeam = program
|
|
655
|
+
.command("red-team")
|
|
656
|
+
.description("Red Team network vulnerability scanner (MCP server for Claude Code)");
|
|
657
|
+
|
|
658
|
+
redTeam
|
|
659
|
+
.command("install")
|
|
660
|
+
.description("Download the scanner and register it as an MCP server")
|
|
661
|
+
.option("--agent <name>", "target agent (only 'claude' is supported)", "claude")
|
|
662
|
+
.requiredOption("--key <ak_xxx>", "access key (ak_<43-char>)")
|
|
663
|
+
.option("--env <env>", "target environment: dev|prod (default: auto-detected from package name)")
|
|
664
|
+
.option("--scope <project|user>", "installation scope: user ($HOME) or project (cwd)", "user")
|
|
665
|
+
.option("--no-verify", "skip /verify call")
|
|
666
|
+
.option("--dry-run", "print planned actions, do not touch disk", false)
|
|
667
|
+
.action(cmdRedTeamInstall);
|
|
668
|
+
|
|
669
|
+
redTeam
|
|
670
|
+
.command("uninstall")
|
|
671
|
+
.description("Remove the MCP server registration and the downloaded scanner")
|
|
672
|
+
.option("--scope <project|user>", "scope: user ($HOME) or project (cwd)", "user")
|
|
673
|
+
.option("--dry-run", "print what would be removed without touching disk", false)
|
|
674
|
+
.action(cmdRedTeamUninstall);
|
|
675
|
+
|
|
629
676
|
program.command("doctor").action(cmdDoctor);
|
|
630
677
|
program.command("list-agents").action(cmdListAgents);
|
|
631
678
|
|