@coworker-jp/aidr 0.0.3 → 0.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coworker-jp/aidr",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "AIDR setup CLI - installs ai-scanner hooks for 19+ AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -155,3 +155,35 @@ export async function fetchOpengrep(destPath) {
155
155
 
156
156
  return { path: destPath, platform, sha256, verified: true };
157
157
  }
158
+
159
+ /**
160
+ * Download coworker-sentinel for the current platform into `destPath`.
161
+ * Uses the presigned S3 URL from the verify response when available,
162
+ * otherwise falls back to the licensed download endpoint.
163
+ * Returns { path, platform, sha256, verified }.
164
+ */
165
+ export async function fetchSentinel(destPath, { accessKey, presignedUrl, env = "prod" } = {}) {
166
+ const platform = detectPlatform();
167
+
168
+ let binUrl;
169
+ if (presignedUrl) {
170
+ // Use presigned URL from verify response (no auth header needed)
171
+ binUrl = presignedUrl;
172
+ accessKey = undefined;
173
+ } else {
174
+ const base = downloadBase(env);
175
+ binUrl = `${base}/sentinel/${platform}`;
176
+ }
177
+
178
+ await fsp.mkdir(path.dirname(destPath), { recursive: true });
179
+ const buf = await fetchBuffer(binUrl, accessKey);
180
+ const sha256 = crypto.createHash("sha256").update(buf).digest("hex");
181
+
182
+ const tmp = `${destPath}.tmp-${process.pid}`;
183
+ await fsp.writeFile(tmp, buf);
184
+ await fsp.chmod(tmp, 0o755);
185
+ await fsp.rename(tmp, destPath);
186
+ await clearQuarantine(destPath);
187
+
188
+ return { path: destPath, platform, sha256, verified: false };
189
+ }
package/src/cli.mjs CHANGED
@@ -89,16 +89,42 @@ async function cmdInstall(opts) {
89
89
  const base = scope === "user" ? home : process.cwd();
90
90
  validateKey(opts.key);
91
91
 
92
+ let verifyInfo = null;
92
93
  if (opts.verify !== false) {
93
94
  try {
94
- const info = await verifyKey(opts.key, opts.env || DEFAULT_ENV);
95
- console.log(`verified: plan=${info.plan} expires_at=${info.expires_at || "n/a"}`);
95
+ verifyInfo = await verifyKey(opts.key, opts.env || DEFAULT_ENV);
96
96
  } catch (e) {
97
97
  console.error(`verify failed: ${e.message}`);
98
98
  process.exit(1);
99
99
  }
100
100
  }
101
101
 
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
+
102
128
  let agents;
103
129
  if (opts.all) {
104
130
  agents = detectInstalled(home);
@@ -246,8 +272,6 @@ async function cmdInstall(opts) {
246
272
  })
247
273
  );
248
274
  await Promise.all(copyOps);
249
-
250
- console.log("Includes Opengrep (LGPL-2.1).");
251
275
  } catch (e) {
252
276
  console.error(`binary fetch failed: ${e.message}`);
253
277
  if (!opts.binaryOnly) console.error("continuing with hook install (use --skip-binary to suppress)");
@@ -298,6 +322,25 @@ async function cmdInstall(opts) {
298
322
  }
299
323
  }
300
324
 
325
+ if (opts.withSentinel) {
326
+ const { installSentinel } = await import("./sentinel.mjs");
327
+ try {
328
+ const platform = (await import("./binary-fetcher.mjs")).detectPlatform();
329
+ const presignedUrl = verifyInfo?.sentinel_download_urls?.[platform] || undefined;
330
+ await installSentinel({
331
+ accessKey: opts.key,
332
+ dryRun: opts.dryRun,
333
+ skipBinary: opts.skipBinary,
334
+ presignedUrl,
335
+ env: opts.env || DEFAULT_ENV,
336
+ noActivate: process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE === "1",
337
+ });
338
+ } catch (e) {
339
+ console.error(`sentinel install failed: ${e.message}`);
340
+ process.exit(1);
341
+ }
342
+ }
343
+
301
344
  if (opts.browserExtension && !opts.dryRun) {
302
345
  const firstAgentName = agents[0];
303
346
  const firstAgent = await loadAgent(firstAgentName);
@@ -517,7 +560,7 @@ export async function run(argv) {
517
560
  program
518
561
  .name("aidr")
519
562
  .description("AIDR setup CLI — installs ai-scanner hooks for AI coding agents")
520
- .version("0.0.3");
563
+ .version("0.0.4");
521
564
 
522
565
  program
523
566
  .command("install")
@@ -536,6 +579,7 @@ export async function run(argv) {
536
579
  .option("--uninstall", "remove all ai-scanner hooks/scripts/binaries for the selected agents (see 'uninstall' command)", false)
537
580
  .option("--scheduled", "enable scheduled endpoint-info collection (creates a cron/systemd/launchd entry; without this flag endpoint-info is never collected)", false)
538
581
  .option("--interval <hours>", "scheduled scan interval in hours (min 1, default 4; requires --scheduled)", "4")
582
+ .option("--with-sentinel", "install coworker-sentinel host-level EDR daemon (Pro/Trial plan; requires sudo)", false)
539
583
  .option("--browser-extension", "install browser extension native messaging host for Shadow IT network monitoring (default: disabled)", false)
540
584
  .action(cmdInstall);
541
585
 
@@ -0,0 +1,239 @@
1
+ /**
2
+ * coworker-sentinel install/uninstall helpers.
3
+ *
4
+ * Requires root (sudo -E) because the daemon runs as a LaunchDaemon (macOS)
5
+ * or systemd service (Linux) to monitor OS-level events system-wide.
6
+ *
7
+ * Install paths (shared with scheduled.mjs):
8
+ * Binary: /opt/coworker/aidr/bin/coworker-sentinel
9
+ * macOS: /Library/LaunchDaemons/jp.coworker.sentinel.plist
10
+ * Linux: /etc/systemd/system/coworker-sentinel.service
11
+ * Env: /etc/aidr/aidr.env (shared with scheduler)
12
+ */
13
+ import fs from "node:fs/promises";
14
+ import path from "node:path";
15
+ import { execFile, spawnSync } from "node:child_process";
16
+ import { promisify } from "node:util";
17
+ import { fetchSentinel, detectPlatform } from "./binary-fetcher.mjs";
18
+ import { SYSTEM_BIN_DIR, SYSTEM_ENV_FILE } from "./scheduled.mjs";
19
+
20
+ const execFileP = promisify(execFile);
21
+
22
+ const ROOT_PREFIX = process.env.AIDR_SCHEDULED_ROOT || "";
23
+
24
+ export const SENTINEL_BIN = path.join(SYSTEM_BIN_DIR, "coworker-sentinel");
25
+ export const SENTINEL_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.sentinel.plist";
26
+ export const SENTINEL_SERVICE = ROOT_PREFIX + "/etc/systemd/system/coworker-sentinel.service";
27
+ export const SENTINEL_LOG_DIR = ROOT_PREFIX + "/var/log/coworker-sentinel";
28
+
29
+ function isRoot() {
30
+ if (process.env.AI_SCANNER_FAKE_ROOT === "1") return true;
31
+ return Boolean(process.getuid && process.getuid() === 0);
32
+ }
33
+
34
+ async function exists(p) {
35
+ try { await fs.access(p); return true; } catch { return false; }
36
+ }
37
+
38
+ export async function isSentinelInstalled() {
39
+ return (
40
+ (await exists(SENTINEL_PLIST)) ||
41
+ (await exists(SENTINEL_SERVICE))
42
+ );
43
+ }
44
+
45
+ function buildLaunchdPlist(envFile) {
46
+ return `<?xml version="1.0" encoding="UTF-8"?>
47
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
48
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
49
+ <plist version="1.0">
50
+ <dict>
51
+ <key>Label</key>
52
+ <string>jp.coworker.sentinel</string>
53
+ <key>ProgramArguments</key>
54
+ <array>
55
+ <string>/bin/sh</string>
56
+ <string>-c</string>
57
+ <string>. ${envFile} &amp;&amp; exec ${SENTINEL_BIN}</string>
58
+ </array>
59
+ <key>RunAtLoad</key>
60
+ <true/>
61
+ <key>KeepAlive</key>
62
+ <true/>
63
+ <key>StandardOutPath</key>
64
+ <string>${SENTINEL_LOG_DIR}/sentinel.log</string>
65
+ <key>StandardErrorPath</key>
66
+ <string>${SENTINEL_LOG_DIR}/sentinel.log</string>
67
+ <key>ThrottleInterval</key>
68
+ <integer>10</integer>
69
+ </dict>
70
+ </plist>
71
+ `;
72
+ }
73
+
74
+ function buildSystemdService(envFile) {
75
+ return `[Unit]
76
+ Description=coworker-sentinel Host EDR Daemon
77
+ After=network.target
78
+ StartLimitIntervalSec=60
79
+ StartLimitBurst=5
80
+
81
+ [Service]
82
+ Type=simple
83
+ EnvironmentFile=${envFile}
84
+ ExecStart=${SENTINEL_BIN}
85
+ Restart=on-failure
86
+ RestartSec=5
87
+ StandardOutput=journal
88
+ StandardError=journal
89
+ SyslogIdentifier=coworker-sentinel
90
+
91
+ [Install]
92
+ WantedBy=multi-user.target
93
+ `;
94
+ }
95
+
96
+ /**
97
+ * Install the sentinel daemon.
98
+ *
99
+ * opts:
100
+ * accessKey — ak_xxx (written to env file if not already present)
101
+ * dryRun — log actions without executing
102
+ * noActivate — skip launchctl/systemctl activation
103
+ * presignedUrl — presigned S3 URL from verify response (optional)
104
+ * env — "dev" | "prod"
105
+ */
106
+ export async function installSentinel(opts = {}) {
107
+ const { accessKey, dryRun = false, skipBinary = false, noActivate = false, presignedUrl, env = "prod" } = opts;
108
+
109
+ if (!isRoot()) {
110
+ throw new Error(
111
+ "installSentinel requires root privileges.\n" +
112
+ "Re-run with: sudo -E npx @coworker-jp/aidr install --key <key> --with-sentinel"
113
+ );
114
+ }
115
+
116
+ const platform = detectPlatform();
117
+ console.error(`[sentinel] Installing coworker-sentinel for ${platform}…`);
118
+
119
+ // 1. Download binary
120
+ if (!dryRun && !skipBinary) {
121
+ await fs.mkdir(SYSTEM_BIN_DIR, { recursive: true });
122
+ console.error(`[sentinel] Downloading binary → ${SENTINEL_BIN}`);
123
+ await fetchSentinel(SENTINEL_BIN, { accessKey, presignedUrl, env });
124
+ } else if (skipBinary) {
125
+ console.error(`[sentinel] Skipping binary download (--skip-binary)`);
126
+ } else {
127
+ console.error(`[sentinel] DRY-RUN: would download → ${SENTINEL_BIN}`);
128
+ }
129
+
130
+ // 2. Create log directory
131
+ if (!dryRun) {
132
+ await fs.mkdir(SENTINEL_LOG_DIR, { recursive: true, mode: 0o755 });
133
+ }
134
+
135
+ // 3. Write env file (append AI_SCANNER_ACCESS_KEY if missing)
136
+ if (accessKey && !dryRun) {
137
+ let content = "";
138
+ try { content = await fs.readFile(SYSTEM_ENV_FILE, "utf8"); } catch { /* first run */ }
139
+ if (!content.includes("AI_SCANNER_ACCESS_KEY")) {
140
+ await fs.mkdir(path.dirname(SYSTEM_ENV_FILE), { recursive: true });
141
+ await fs.appendFile(SYSTEM_ENV_FILE, `\nAI_SCANNER_ACCESS_KEY=${accessKey}\n`);
142
+ await fs.chmod(SYSTEM_ENV_FILE, 0o600);
143
+ }
144
+ }
145
+
146
+ // 4. Write service/plist
147
+ if (process.platform === "darwin") {
148
+ await _installMacos(dryRun, noActivate);
149
+ } else {
150
+ await _installLinux(dryRun, noActivate);
151
+ }
152
+
153
+ console.error("[sentinel] Installation complete.");
154
+ }
155
+
156
+ async function _installMacos(dryRun, noActivate) {
157
+ const plistContent = buildLaunchdPlist(SYSTEM_ENV_FILE);
158
+ if (!dryRun) {
159
+ await fs.mkdir(path.dirname(SENTINEL_PLIST), { recursive: true });
160
+ await fs.writeFile(SENTINEL_PLIST, plistContent, { mode: 0o644 });
161
+ console.error(`[sentinel] Written: ${SENTINEL_PLIST}`);
162
+
163
+ if (!noActivate && process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE !== "1") {
164
+ // Unload existing (ignore errors) then load
165
+ spawnSync("launchctl", ["unload", SENTINEL_PLIST], { stdio: "ignore" });
166
+ const load = spawnSync("launchctl", ["load", "-w", SENTINEL_PLIST]);
167
+ if (load.status !== 0) {
168
+ console.warn("[sentinel] launchctl load failed — daemon may need manual start");
169
+ } else {
170
+ console.error("[sentinel] Daemon loaded via launchctl");
171
+ }
172
+ }
173
+ } else {
174
+ console.error(`[sentinel] DRY-RUN: would write ${SENTINEL_PLIST}`);
175
+ }
176
+ }
177
+
178
+ async function _installLinux(dryRun, noActivate) {
179
+ const serviceContent = buildSystemdService(SYSTEM_ENV_FILE);
180
+ const serviceDir = path.dirname(SENTINEL_SERVICE);
181
+ if (!dryRun) {
182
+ await fs.mkdir(serviceDir, { recursive: true });
183
+ await fs.writeFile(SENTINEL_SERVICE, serviceContent, { mode: 0o644 });
184
+ console.error(`[sentinel] Written: ${SENTINEL_SERVICE}`);
185
+
186
+ if (!noActivate && process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE !== "1") {
187
+ spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
188
+ spawnSync("systemctl", ["enable", "--now", "coworker-sentinel"], { stdio: "ignore" });
189
+ console.error("[sentinel] Enabled and started via systemctl");
190
+ }
191
+ } else {
192
+ console.error(`[sentinel] DRY-RUN: would write ${SENTINEL_SERVICE}`);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Uninstall the sentinel daemon.
198
+ */
199
+ export async function uninstallSentinel({ dryRun = false } = {}) {
200
+ if (!isRoot()) {
201
+ throw new Error("uninstallSentinel requires root privileges.");
202
+ }
203
+
204
+ console.error("[sentinel] Uninstalling coworker-sentinel…");
205
+
206
+ if (process.platform === "darwin") {
207
+ if (await exists(SENTINEL_PLIST)) {
208
+ if (!dryRun) {
209
+ spawnSync("launchctl", ["unload", SENTINEL_PLIST], { stdio: "ignore" });
210
+ await fs.unlink(SENTINEL_PLIST);
211
+ console.error(`[sentinel] Removed: ${SENTINEL_PLIST}`);
212
+ } else {
213
+ console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_PLIST}`);
214
+ }
215
+ }
216
+ } else {
217
+ if (await exists(SENTINEL_SERVICE)) {
218
+ if (!dryRun) {
219
+ spawnSync("systemctl", ["disable", "--now", "coworker-sentinel"], { stdio: "ignore" });
220
+ await fs.unlink(SENTINEL_SERVICE);
221
+ spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
222
+ console.error(`[sentinel] Removed: ${SENTINEL_SERVICE}`);
223
+ } else {
224
+ console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_SERVICE}`);
225
+ }
226
+ }
227
+ }
228
+
229
+ if (await exists(SENTINEL_BIN)) {
230
+ if (!dryRun) {
231
+ await fs.unlink(SENTINEL_BIN);
232
+ console.error(`[sentinel] Removed: ${SENTINEL_BIN}`);
233
+ } else {
234
+ console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_BIN}`);
235
+ }
236
+ }
237
+
238
+ console.error("[sentinel] Uninstall complete.");
239
+ }
package/src/templates.mjs CHANGED
@@ -1,7 +1,7 @@
1
- // SOURCE: docker/user-frontend/src/lib/scanner-templates.ts
2
- // ESM port of the scanner hook templates. Keep in sync with the TS source.
3
- // Templates are byte-identical to the canonical .{agent}/ files in ai-scanner
4
- // repo (Claude / Cursor / Windsurf / Kiro) — only the access key value differs.
1
+ // Canonical source for the scanner hook templates installed by aidr.
2
+ // Each template emits a script byte-identical to the corresponding file under
3
+ // .{agent}/ at the root of this repo (Claude / Cursor / Windsurf / Kiro);
4
+ // only the access key and the verify endpoint differ.
5
5
 
6
6
  import { buildClaudeSettings, buildGeminiSettings } from "./merge.mjs";
7
7
 
package/src/verify.mjs CHANGED
@@ -42,6 +42,10 @@ export async function verifyKey(accessKey, env = "prod") {
42
42
  status: body.status,
43
43
  plan: body.plan || "standard",
44
44
  expires_at: body.expires_at || null,
45
+ edr_enabled: body.edr_enabled || false,
46
+ latest_sentinel_version: body.latest_sentinel_version || null,
47
+ sentinel_download_urls: body.sentinel_download_urls || {},
48
+ sentinel_sha256: body.sentinel_sha256 || {},
45
49
  };
46
50
  }
47
51