@coworker-jp/aidr 0.1.11 → 0.1.75

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/src/sentinel.mjs CHANGED
@@ -1,27 +1,34 @@
1
1
  /**
2
- * coworker-sentinel install/uninstall helpers.
2
+ * LEGACY coworker-sentinel cleanup helpers.
3
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.
4
+ * aidr no longer installs coworker-sentinel (the `--with-sentinel` flag is
5
+ * retired). Sentinel now ships via OS-native installers:
6
+ * Windows: coworker-sentinel-setup.exe (Inno Setup, own uninstaller)
7
+ * macOS: CoworkerSentinel.dmg (the app self-installs/uninstalls)
8
+ * Linux: coworker-sentinel.deb (dpkg postrm owns the cleanup;
9
+ * unit lives at /lib/systemd/system/coworker-sentinel.service)
6
10
  *
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)
11
+ * What remains here is uninstall-only: `aidr uninstall` sweeps the artifacts
12
+ * an old npx-era `aidr install --with-sentinel` wrote on Linux —
13
+ * /etc/systemd/system/coworker-sentinel.service
14
+ * /opt/coworker/aidr/bin/coworker-sentinel (+ ai-scanner)
15
+ * /etc/aidr/sentinel-settings.json, /etc/aidr/aidr.env
16
+ * /var/log/coworker/sentinel.log (+ logrotate config)
17
+ *
18
+ * It must NEVER touch a deb-owned install: the deb's systemd unit is under
19
+ * /lib/systemd/system (not /etc/systemd/system), and when a deb install is
20
+ * present the shared files under /etc/aidr and the OS-trusted root CA are
21
+ * left alone (the deb daemon keeps using them; dpkg purge removes them).
12
22
  */
13
23
  import fs from "node:fs/promises";
14
24
  import path from "node:path";
15
25
  import { execFile, spawnSync } from "node:child_process";
16
- import { resolveInvokerUser } from "./invoker-resolve.mjs";
17
26
  import { promisify } from "node:util";
18
- import { fetchSentinel, detectPlatform } from "./binary-fetcher.mjs";
19
27
  import {
20
28
  SYSTEM_BIN_DIR,
29
+ SYSTEM_BIN,
21
30
  SYSTEM_ENV_FILE,
22
31
  SENTINEL_LOG_FILE,
23
- ensureSystemLogDir,
24
- writeLogrotateConfig,
25
32
  removeSystemLog,
26
33
  } from "./scheduled.mjs";
27
34
 
@@ -29,13 +36,30 @@ const execFileP = promisify(execFile);
29
36
 
30
37
  const ROOT_PREFIX = process.env.AIDR_SCHEDULED_ROOT || "";
31
38
 
39
+ // Legacy npx-era binary location (never used by the deb / setup.exe / DMG).
32
40
  export const SENTINEL_BIN = path.join(
33
41
  SYSTEM_BIN_DIR,
34
42
  process.platform === "win32" ? "coworker-sentinel.exe" : "coworker-sentinel",
35
43
  );
44
+ // Daemon-owned settings file (endpoint-info collection toggle etc.). Shared
45
+ // between the legacy npx daemon and a deb-installed daemon — only removed
46
+ // when no deb install is present.
47
+ export const SENTINEL_SETTINGS_FILE = path.join(
48
+ path.dirname(SYSTEM_ENV_FILE),
49
+ "sentinel-settings.json",
50
+ );
36
51
  export const SENTINEL_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.sentinel.plist";
52
+ // Legacy npx-written unit. The deb ships its unit at DEB_SENTINEL_SERVICE —
53
+ // the two paths are how we tell the layouts apart.
37
54
  export const SENTINEL_SERVICE = ROOT_PREFIX + "/etc/systemd/system/coworker-sentinel.service";
38
- export const SENTINEL_WIN_SERVICE = "CoworkerSentinel";
55
+ export const DEB_SENTINEL_SERVICE = ROOT_PREFIX + "/lib/systemd/system/coworker-sentinel.service";
56
+
57
+ export const MACOS_DMG_HINT =
58
+ "[sentinel] macOS の coworker-sentinel は DMG (CoworkerSentinel.app) から導入します。\n" +
59
+ "[sentinel] ユーザーポータルの Download ページ、または GET /download/setup/macos から\n" +
60
+ "[sentinel] CoworkerSentinel.dmg をダウンロードし、Applications にドラッグ → 起動 →\n" +
61
+ "[sentinel] アクセスキーを入力 → 管理者パスワードを入力してください。npx 経由の\n" +
62
+ "[sentinel] インストール/アンインストールは macOS では行われません。";
39
63
 
40
64
  function isRoot() {
41
65
  if (process.env.AI_SCANNER_FAKE_ROOT === "1") return true;
@@ -54,270 +78,109 @@ async function exists(p) {
54
78
  try { await fs.access(p); return true; } catch { return false; }
55
79
  }
56
80
 
81
+ /**
82
+ * True only for the LEGACY npx-installed layout that aidr itself once wrote
83
+ * (Linux systemd unit under /etc/systemd/system). A deb install (unit under
84
+ * /lib/systemd/system) must NOT match — its cleanup belongs to dpkg postrm.
85
+ * macOS (DMG-owned) and Windows (setup.exe-owned, and indistinguishable from
86
+ * the npx-era layout — same %ProgramFiles%\coworker\aidr\bin + service name)
87
+ * always report false: aidr never uninstalls sentinel there.
88
+ */
57
89
  export async function isSentinelInstalled() {
58
- if (process.platform === "win32") {
59
- try {
60
- return spawnSync("sc", ["query", SENTINEL_WIN_SERVICE], { stdio: "ignore" }).status === 0;
61
- } catch {
62
- return false;
63
- }
64
- }
65
- return (
66
- (await exists(SENTINEL_PLIST)) ||
67
- (await exists(SENTINEL_SERVICE))
68
- );
69
- }
70
-
71
- function buildLaunchdPlist(envFile) {
72
- return `<?xml version="1.0" encoding="UTF-8"?>
73
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
74
- "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
75
- <plist version="1.0">
76
- <dict>
77
- <key>Label</key>
78
- <string>jp.coworker.sentinel</string>
79
- <key>ProgramArguments</key>
80
- <array>
81
- <string>/bin/sh</string>
82
- <string>-c</string>
83
- <string>set -a; . ${envFile}; set +a; exec ${SENTINEL_BIN}</string>
84
- </array>
85
- <key>RunAtLoad</key>
86
- <true/>
87
- <key>KeepAlive</key>
88
- <true/>
89
- <key>StandardOutPath</key>
90
- <string>${SENTINEL_LOG_FILE}</string>
91
- <key>StandardErrorPath</key>
92
- <string>${SENTINEL_LOG_FILE}</string>
93
- <key>ThrottleInterval</key>
94
- <integer>10</integer>
95
- </dict>
96
- </plist>
97
- `;
98
- }
99
-
100
- function buildSystemdService(envFile) {
101
- return `[Unit]
102
- Description=coworker-sentinel Host EDR Daemon
103
- After=network.target
104
- StartLimitIntervalSec=60
105
- StartLimitBurst=5
106
-
107
- [Service]
108
- Type=simple
109
- EnvironmentFile=${envFile}
110
- ExecStart=${SENTINEL_BIN}
111
- Restart=on-failure
112
- RestartSec=5
113
- StandardOutput=append:${SENTINEL_LOG_FILE}
114
- StandardError=append:${SENTINEL_LOG_FILE}
115
- SyslogIdentifier=coworker-sentinel
116
-
117
- [Install]
118
- WantedBy=multi-user.target
119
- `;
90
+ if (process.platform === "darwin" || process.platform === "win32") return false;
91
+ return exists(SENTINEL_SERVICE);
120
92
  }
121
93
 
122
94
  /**
123
- * Install the sentinel daemon.
95
+ * Remove a LEGACY npx-installed sentinel daemon (Linux only).
124
96
  *
125
- * opts:
126
- * accessKey — ak_xxx (written to env file if not already present)
127
- * dryRun — log actions without executing
128
- * noActivate — skip launchctl/systemctl activation
129
- * presignedUrl — presigned S3 URL from verify response (optional)
130
- * env — "dev" | "prod"
97
+ * Deb coexistence rules:
98
+ * - the deb unit (/lib/systemd/system) and deb-owned files are never touched;
99
+ * - when a deb install is present, the shared /etc/aidr files, the system
100
+ * ai-scanner copy, the root CA, and the log file are all preserved
101
+ * (the deb daemon still uses them) — only the stale legacy unit and the
102
+ * legacy /opt binary are swept;
103
+ * - `systemctl disable --now` is only issued when no deb unit exists, so a
104
+ * running deb service is never stopped.
131
105
  */
132
- export async function installSentinel(opts = {}) {
133
- const { accessKey, dryRun = false, skipBinary = false, noActivate = false, presignedUrl, dllPresignedUrl, env = "prod" } = opts;
134
-
135
- if (!isRoot()) {
136
- throw new Error(
137
- process.platform === "win32"
138
- ? "installSentinel requires an elevated (Administrator) shell.\n" +
139
- "Re-run from an Administrator PowerShell/cmd: npx @coworker-jp/aidr install --key <key> --with-sentinel"
140
- : "installSentinel requires root privileges.\n" +
141
- "Re-run with: sudo -E npx @coworker-jp/aidr install --key <key> --with-sentinel",
106
+ export async function uninstallSentinel({ dryRun = false } = {}) {
107
+ // macOS: DMG-based. Uninstall is done from the CoworkerSentinel.app's
108
+ // Uninstall button (which unloads the daemon, runs uninstall-ca, and
109
+ // deactivates the system extension behind one admin prompt). No-op + hint.
110
+ if (process.platform === "darwin") {
111
+ console.error(MACOS_DMG_HINT);
112
+ console.error(
113
+ "[sentinel] アンインストールは CoworkerSentinel.app の「Uninstall」ボタンから行ってください。",
142
114
  );
115
+ return;
143
116
  }
144
117
 
145
- const platform = detectPlatform();
146
- console.error(`[sentinel] Installing coworker-sentinel for ${platform}…`);
147
-
148
- // 1. Download binary
149
- if (!dryRun && !skipBinary) {
150
- await fs.mkdir(SYSTEM_BIN_DIR, { recursive: true });
151
- console.error(`[sentinel] Downloading binary → ${SENTINEL_BIN}`);
152
- await fetchSentinel(SENTINEL_BIN, { accessKey, presignedUrl, dllPresignedUrl, env });
153
- } else if (skipBinary) {
154
- console.error(`[sentinel] Skipping binary download (--skip-binary)`);
155
- } else {
156
- console.error(`[sentinel] DRY-RUN: would download → ${SENTINEL_BIN}`);
157
- }
158
-
159
- // 2. Create the world-readable log dir + touch sentinel.log + drop the
160
- // shared logrotate config (covering both daemons; idempotent).
161
- await ensureSystemLogDir(SENTINEL_LOG_FILE, { dryRun });
162
- await writeLogrotateConfig({ dryRun });
163
-
164
- // 3. Write env file (append AI_SCANNER_ACCESS_KEY + AI_SCANNER_INVOKER_USER
165
- // if missing). When --with-sentinel runs alongside --scheduled,
166
- // scheduled.mjs's writeEnvFile has already written both lines and
167
- // we no-op. When --with-sentinel runs standalone (no --scheduled),
168
- // we are the only writer of aidr.env and must capture SUDO_USER
169
- // ourselves; otherwise the daemon (running as root with no useful
170
- // git/Apple/USER) reports `unknown`. Identity resolution itself
171
- // (Apple ID / git email / username) happens at scan time inside
172
- // the Rust binary using `sudo -u <invoker>`.
173
- if (accessKey && !dryRun) {
174
- let content = "";
175
- try { content = await fs.readFile(SYSTEM_ENV_FILE, "utf8"); } catch { /* first run */ }
176
- let appended = "";
177
- if (!content.includes("AI_SCANNER_ACCESS_KEY")) {
178
- appended += `\nAI_SCANNER_ACCESS_KEY=${accessKey}\n`;
179
- }
180
- if (!content.includes("AI_SCANNER_INVOKER_USER")) {
181
- const invoker = resolveInvokerUser();
182
- if (invoker) {
183
- appended += `AI_SCANNER_INVOKER_USER=${invoker}\n`;
184
- }
185
- }
186
- if (appended) {
187
- await fs.mkdir(path.dirname(SYSTEM_ENV_FILE), { recursive: true });
188
- await fs.appendFile(SYSTEM_ENV_FILE, appended);
189
- await fs.chmod(SYSTEM_ENV_FILE, 0o600);
190
- }
191
- }
192
-
193
- // 4. Write service/plist (or register the Windows service)
118
+ // Windows: setup.exe-based. Its uninstaller (Add/Remove Programs) runs
119
+ // uninstall-service + uninstall-ca itself; the npx-era layout is identical
120
+ // (same bin dir + service name), so touching it here would corrupt a
121
+ // setup.exe install. No-op + hint.
194
122
  if (process.platform === "win32") {
195
- await _installWindows(dryRun, noActivate);
196
- } else if (process.platform === "darwin") {
197
- await _installMacos(dryRun, noActivate);
198
- } else {
199
- await _installLinux(dryRun, noActivate);
200
- }
201
-
202
- console.error("[sentinel] Installation complete.");
203
- }
204
-
205
- async function _installWindows(dryRun, noActivate) {
206
- if (dryRun) {
207
- console.error(`[sentinel] DRY-RUN: would run \`${SENTINEL_BIN} install-ca\` + install-service`);
123
+ console.error(
124
+ "[sentinel] Windows の coworker-sentinel は setup.exe で管理されます。\n" +
125
+ "[sentinel] アンインストールは「設定 > アプリ」(Add/Remove Programs) から行ってください。",
126
+ );
208
127
  return;
209
128
  }
210
- // The service (LocalSystem) reads AI_SCANNER_ACCESS_KEY etc. from
211
- // %ProgramData%\coworker\aidr.env, written by installSentinel step 3.
212
- // install-ca adds the DLP root to the OS trust store; install-service
213
- // registers the auto-start service and starts it immediately.
214
- await execFileP(SENTINEL_BIN, ["install-ca"]);
215
- console.error("[sentinel] Root CA installed into the Windows trust store");
216
- if (!noActivate && process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE !== "1") {
217
- await execFileP(SENTINEL_BIN, ["install-service"]);
218
- console.error(`[sentinel] Service registered and started (${SENTINEL_WIN_SERVICE})`);
219
- } else {
220
- console.error("[sentinel] --no-activate: skipping service registration");
221
- }
222
- }
223
129
 
224
- async function _uninstallWindows(dryRun) {
225
- if (dryRun) {
226
- console.error("[sentinel] DRY-RUN: would run uninstall-service + uninstall-ca");
227
- return;
228
- }
229
- // Order mirrors the CLI dispatch: stop/remove the service (so the exe isn't
230
- // in use), then drop the MITM root CA so no dangling trust anchor remains.
231
- try {
232
- await execFileP(SENTINEL_BIN, ["uninstall-service"]);
233
- } catch (e) {
234
- console.warn("[sentinel] uninstall-service:", e.message);
235
- }
236
- try {
237
- await execFileP(SENTINEL_BIN, ["uninstall-ca"]);
238
- } catch (e) {
239
- console.warn("[sentinel] uninstall-ca:", e.message);
130
+ if (!isRoot()) {
131
+ throw new Error("uninstallSentinel requires root privileges.");
240
132
  }
241
- }
242
133
 
243
- async function _installMacos(dryRun, noActivate) {
244
- const plistContent = buildLaunchdPlist(SYSTEM_ENV_FILE);
245
- if (!dryRun) {
246
- await fs.mkdir(path.dirname(SENTINEL_PLIST), { recursive: true });
247
- await fs.writeFile(SENTINEL_PLIST, plistContent, { mode: 0o644 });
248
- console.error(`[sentinel] Written: ${SENTINEL_PLIST}`);
134
+ console.error("[sentinel] Removing legacy npx-installed coworker-sentinel…");
249
135
 
250
- if (!noActivate && process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE !== "1") {
251
- // Modern launchctl API (bootstrap/bootout). Mirrors scheduled.mjs so
252
- // both daemons share one mental model. Idempotent: bootout-before-
253
- // bootstrap absorbs prior loads regardless of which API installed them.
254
- try { await execFileP("launchctl", ["bootout", "system", SENTINEL_PLIST]); } catch { /* not loaded */ }
255
- try {
256
- await execFileP("launchctl", ["bootstrap", "system", SENTINEL_PLIST]);
257
- console.error("[sentinel] Daemon loaded via launchctl");
258
- } catch (e) {
259
- console.warn("[sentinel] launchctl bootstrap failed — daemon may need manual start:", e.message);
260
- }
261
- }
262
- } else {
263
- console.error(`[sentinel] DRY-RUN: would write ${SENTINEL_PLIST}`);
136
+ const debInstalled = await exists(DEB_SENTINEL_SERVICE);
137
+ if (debInstalled) {
138
+ console.error(
139
+ `[sentinel] deb install detected (${DEB_SENTINEL_SERVICE}) — removing only the stale legacy unit/binary; deb-owned files are untouched.`,
140
+ );
264
141
  }
265
- }
266
-
267
- async function _installLinux(dryRun, noActivate) {
268
- const serviceContent = buildSystemdService(SYSTEM_ENV_FILE);
269
- const serviceDir = path.dirname(SENTINEL_SERVICE);
270
- if (!dryRun) {
271
- await fs.mkdir(serviceDir, { recursive: true });
272
- await fs.writeFile(SENTINEL_SERVICE, serviceContent, { mode: 0o644 });
273
- console.error(`[sentinel] Written: ${SENTINEL_SERVICE}`);
274
142
 
275
- if (!noActivate && process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE !== "1") {
143
+ if (await exists(SENTINEL_SERVICE)) {
144
+ if (!dryRun) {
145
+ // Only stop/disable when no deb unit exists: `disable --now` acts on
146
+ // the service name and would stop a running deb daemon.
147
+ if (!debInstalled) {
148
+ spawnSync("systemctl", ["disable", "--now", "coworker-sentinel"], { stdio: "ignore" });
149
+ }
150
+ await fs.unlink(SENTINEL_SERVICE);
276
151
  spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
277
- spawnSync("systemctl", ["enable", "--now", "coworker-sentinel"], { stdio: "ignore" });
278
- console.error("[sentinel] Enabled and started via systemctl");
152
+ console.error(`[sentinel] Removed: ${SENTINEL_SERVICE}`);
153
+ } else {
154
+ console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_SERVICE}`);
279
155
  }
280
- } else {
281
- console.error(`[sentinel] DRY-RUN: would write ${SENTINEL_SERVICE}`);
282
156
  }
283
- }
284
157
 
285
- /**
286
- * Uninstall the sentinel daemon.
287
- */
288
- export async function uninstallSentinel({ dryRun = false } = {}) {
289
- if (!isRoot()) {
290
- throw new Error("uninstallSentinel requires root privileges.");
291
- }
292
-
293
- console.error("[sentinel] Uninstalling coworker-sentinel…");
294
-
295
- if (process.platform === "win32") {
296
- await _uninstallWindows(dryRun);
297
- } else if (process.platform === "darwin") {
298
- if (await exists(SENTINEL_PLIST)) {
299
- if (!dryRun) {
300
- // Modern launchctl API; mirrors scheduled.mjs's uninstallScheduled.
301
- try { await execFileP("launchctl", ["bootout", "system", SENTINEL_PLIST]); } catch { /* not loaded */ }
302
- await fs.unlink(SENTINEL_PLIST);
303
- console.error(`[sentinel] Removed: ${SENTINEL_PLIST}`);
304
- } else {
305
- console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_PLIST}`);
306
- }
307
- }
308
- } else {
309
- if (await exists(SENTINEL_SERVICE)) {
310
- if (!dryRun) {
311
- spawnSync("systemctl", ["disable", "--now", "coworker-sentinel"], { stdio: "ignore" });
312
- await fs.unlink(SENTINEL_SERVICE);
313
- spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
314
- console.error(`[sentinel] Removed: ${SENTINEL_SERVICE}`);
315
- } else {
316
- console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_SERVICE}`);
158
+ // The legacy daemon auto-installed the DLP root CA into the OS trust store
159
+ // at startup, so uninstall must drop that trust anchor too — otherwise a
160
+ // MITM-capable root stays trusted after the daemon is gone. Must run
161
+ // BEFORE the binary is deleted (the binary is the uninstaller), and
162
+ // best-effort: a never-elevated install has no CA, and a pre-DLP binary
163
+ // doesn't know the subcommand and would start the daemon instead — the
164
+ // timeout kills that instead of hanging the uninstall. Skipped when a deb
165
+ // install is present: the deb daemon relies on that same trust anchor.
166
+ if (!debInstalled && (await exists(SENTINEL_BIN))) {
167
+ if (!dryRun) {
168
+ try {
169
+ await execFileP(SENTINEL_BIN, ["uninstall-ca"], {
170
+ timeout: 15_000,
171
+ killSignal: "SIGKILL",
172
+ });
173
+ console.error("[sentinel] Root CA removed from the OS trust store");
174
+ } catch (e) {
175
+ console.warn("[sentinel] uninstall-ca:", e.message);
317
176
  }
177
+ } else {
178
+ console.error(`[sentinel] DRY-RUN: would run \`${SENTINEL_BIN} uninstall-ca\``);
318
179
  }
319
180
  }
320
181
 
182
+ // The legacy binary itself is npx-owned — always removed (the deb installs
183
+ // its binary elsewhere).
321
184
  if (await exists(SENTINEL_BIN)) {
322
185
  if (!dryRun) {
323
186
  await fs.unlink(SENTINEL_BIN);
@@ -327,23 +190,37 @@ export async function uninstallSentinel({ dryRun = false } = {}) {
327
190
  }
328
191
  }
329
192
 
330
- // Windows ships a sibling WinDivert.dll next to the exe — remove it too.
331
- if (process.platform === "win32") {
332
- const dll = path.join(path.dirname(SENTINEL_BIN), "WinDivert.dll");
333
- if (await exists(dll)) {
334
- if (!dryRun) {
335
- await fs.unlink(dll);
336
- console.error(`[sentinel] Removed: ${dll}`);
337
- } else {
338
- console.error(`[sentinel] DRY-RUN: would remove ${dll}`);
193
+ // Shared artifacts the legacy daemon owned: its settings file
194
+ // (sentinel-settings.json — endpoint-info collection toggle), the
195
+ // ai-scanner binary in SYSTEM_BIN_DIR that it either self-downloaded or
196
+ // that the old `aidr install --with-sentinel` placed for it, and the shared
197
+ // env file /etc/aidr/aidr.env (root-owned 0600 access key). With no daemon
198
+ // left, nothing consumes them — leaving the key behind is worse hygiene
199
+ // than re-entering it on reinstall. With a deb install present they are
200
+ // still live (the deb daemon reads them) and must be preserved; dpkg purge
201
+ // removes them when the deb goes. Best-effort removal.
202
+ if (!debInstalled) {
203
+ for (const f of [SENTINEL_SETTINGS_FILE, SYSTEM_BIN, SYSTEM_ENV_FILE]) {
204
+ if (await exists(f)) {
205
+ if (!dryRun) {
206
+ try {
207
+ await fs.unlink(f);
208
+ console.error(`[sentinel] Removed: ${f}`);
209
+ } catch (e) {
210
+ console.warn(`[sentinel] could not remove ${f}: ${e.message}`);
211
+ }
212
+ } else {
213
+ console.error(`[sentinel] DRY-RUN: would remove ${f}`);
214
+ }
339
215
  }
340
216
  }
341
- }
342
217
 
343
- // Drop the sentinel log file. removeSystemLog also rmdirs SYSTEM_LOG_DIR
344
- // and deletes /etc/logrotate.d/coworker if scheduled.log is also gone
345
- // (i.e. the other daemon is uninstalled).
346
- await removeSystemLog(SENTINEL_LOG_FILE, { dryRun });
218
+ // Drop the sentinel log file. removeSystemLog also rmdirs SYSTEM_LOG_DIR
219
+ // and deletes /etc/logrotate.d/coworker if scheduled.log is also gone
220
+ // (i.e. the other legacy daemon is uninstalled). A deb daemon may still
221
+ // be appending to the log, so this too is deb-gated.
222
+ await removeSystemLog(SENTINEL_LOG_FILE, { dryRun });
223
+ }
347
224
 
348
- console.error("[sentinel] Uninstall complete.");
225
+ console.error("[sentinel] Legacy cleanup complete.");
349
226
  }
package/src/templates.mjs CHANGED
@@ -18,7 +18,7 @@ export function getStartScannerSh(downloadBase = "https://pytd2jd5p7.execute-api
18
18
  return `#!/bin/bash
19
19
  # SessionStart hook: Setup ai-scanner for the current platform
20
20
  # - Linux (x86_64 / aarch64): download native binary from S3
21
- # - macOS (arm64 / Apple Silicon): download native binary from S3 (ad-hoc signed)
21
+ # - macOS (arm64 + x86_64): download the universal native binary from S3 (ad-hoc signed)
22
22
  # - Binary is unsigned by 3rd party dev cert — quarantine xattr is stripped after download
23
23
  # - Fail-open: never block Claude Code startup
24
24
  BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
@@ -84,11 +84,12 @@ case "$OS" in
84
84
  ;;
85
85
  Darwin)
86
86
  case "$ARCH" in
87
- arm64) PLATFORM_SLUG="darwin-aarch64" ;;
88
- x86_64)
89
- echo "Warning: ai-scanner does not support Intel Mac (x86_64). Apple Silicon required." >&2
90
- exit 0
91
- ;;
87
+ # Both arches map to the SAME slug on purpose: the object under
88
+ # darwin-aarch64 is a universal (fat) Mach-O with an arm64 and an
89
+ # x86_64 slice, so one download serves Apple Silicon and Intel. The
90
+ # slug is a wire contract with the verify Lambda's SCANNER_PLATFORMS
91
+ # — only its contents became universal, never its name.
92
+ arm64|x86_64) PLATFORM_SLUG="darwin-aarch64" ;;
92
93
  *)
93
94
  echo "Warning: Unsupported macOS architecture: $ARCH" >&2
94
95
  exit 0
package/src/verify.mjs CHANGED
@@ -38,14 +38,13 @@ export async function verifyKey(accessKey, env = "prod") {
38
38
  if (body.status !== "active") {
39
39
  throw new Error(`access key status is '${body.status || "unknown"}' (expected 'active')`);
40
40
  }
41
+ // The server may also return sentinel-related fields (edr_enabled,
42
+ // sentinel_download_urls, …) for the OS-native installers; aidr is
43
+ // hooks-only and ignores them (tolerant parsing — unknown fields are fine).
41
44
  return {
42
45
  status: body.status,
43
46
  plan: body.plan || "standard",
44
47
  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 || {},
49
48
  };
50
49
  }
51
50
 
@@ -1,83 +0,0 @@
1
- import { writeFileSync, mkdirSync, readFileSync, chmodSync } from "fs";
2
- import { join } from "path";
3
-
4
- // Extension ID will be determined after Chrome Web Store publication.
5
- // For now use a placeholder that operators can override via AIDR_EXTENSION_ID env var.
6
- const DEFAULT_EXTENSION_ID = "AIDR_EXTENSION_ID_PLACEHOLDER";
7
-
8
- export async function installBrowserExtension(homeDir, binaryPath) {
9
- const extensionId = process.env.AIDR_EXTENSION_ID || DEFAULT_EXTENSION_ID;
10
- const platform = process.platform;
11
-
12
- // 1. Write Native Messaging Host manifests for Chrome/Edge/Brave/Chromium
13
-
14
- // Generate a wrapper script so the binary is invoked in native-messaging-host mode
15
- const binDir = join(homeDir, ".claude", "bin");
16
- mkdirSync(binDir, { recursive: true });
17
- const wrapperPath = join(binDir, "ai-scanner-nativehost.sh");
18
- const wrapperContent = `#!/bin/sh\nexec "${binaryPath}" native-messaging-host "$@"\n`;
19
- try {
20
- writeFileSync(wrapperPath, wrapperContent, { mode: 0o755 });
21
- } catch (_e) {
22
- // fall back to binary path directly
23
- }
24
-
25
- const nativeManifest = {
26
- name: "jp.coworker.aidr.browser",
27
- description: "AIDR Shadow IT Monitor bridge",
28
- path: wrapperPath,
29
- type: "stdio",
30
- allowed_origins: [`chrome-extension://${extensionId}/`],
31
- };
32
-
33
- const chromePaths = getNativeMessagingPaths(platform, homeDir);
34
- for (const dir of chromePaths) {
35
- try {
36
- mkdirSync(dir, { recursive: true });
37
- writeFileSync(
38
- join(dir, "jp.coworker.aidr.browser.json"),
39
- JSON.stringify(nativeManifest, null, 2)
40
- );
41
- } catch (_e) {
42
- // fail-open: skip paths we can't write
43
- }
44
- }
45
-
46
- // 2. Record in ~/.claude/ai-scanner-config.json
47
- const configPath = join(homeDir, ".claude", "ai-scanner-config.json");
48
- let config = {};
49
- try {
50
- config = JSON.parse(readFileSync(configPath, "utf8"));
51
- } catch (_e) {
52
- // file doesn't exist yet or is unreadable; start fresh
53
- }
54
- config.browser_extension = true;
55
- config.extension_id = extensionId;
56
- try {
57
- writeFileSync(configPath, JSON.stringify(config, null, 2));
58
- } catch (_e) {
59
- // fail-open: config write is best-effort
60
- }
61
-
62
- console.log("✓ Browser extension native messaging host installed");
63
- console.log(` Extension ID: ${extensionId}`);
64
- console.log(" Open Chrome and install the AIDR Shadow IT Monitor extension to complete setup.");
65
- }
66
-
67
- function getNativeMessagingPaths(platform, homeDir) {
68
- if (platform === "darwin") {
69
- return [
70
- join(homeDir, "Library/Application Support/Google/Chrome/NativeMessagingHosts"),
71
- join(homeDir, "Library/Application Support/Microsoft Edge/NativeMessagingHosts"),
72
- join(homeDir, "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts"),
73
- join(homeDir, "Library/Application Support/Chromium/NativeMessagingHosts"),
74
- ];
75
- } else {
76
- return [
77
- join(homeDir, ".config/google-chrome/NativeMessagingHosts"),
78
- join(homeDir, ".config/microsoft-edge/NativeMessagingHosts"),
79
- join(homeDir, ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts"),
80
- join(homeDir, ".config/chromium/NativeMessagingHosts"),
81
- ];
82
- }
83
- }