@coworker-jp/aidr 0.1.9 → 0.1.11

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.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "AIDR setup CLI - installs ai-scanner hooks for 19+ AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@ export function detectPlatform() {
14
14
  if (os === "linux" && arch === "x64") return "linux-x86_64";
15
15
  if (os === "linux" && arch === "arm64") return "linux-aarch64";
16
16
  if (os === "darwin" && arch === "arm64") return "darwin-aarch64";
17
+ if (os === "win32" && arch === "x64") return "windows-x86_64";
17
18
  if (os === "darwin" && arch === "x64") {
18
19
  throw new Error("Intel Mac (darwin-x86_64) is not supported. Apple Silicon only.");
19
20
  }
@@ -162,28 +163,50 @@ export async function fetchOpengrep(destPath) {
162
163
  * otherwise falls back to the licensed download endpoint.
163
164
  * Returns { path, platform, sha256, verified }.
164
165
  */
165
- export async function fetchSentinel(destPath, { accessKey, presignedUrl, env = "prod" } = {}) {
166
+ export async function fetchSentinel(
167
+ destPath,
168
+ { accessKey, presignedUrl, dllPresignedUrl, env = "prod" } = {},
169
+ ) {
166
170
  const platform = detectPlatform();
167
171
 
168
172
  let binUrl;
173
+ let binAuthKey = accessKey;
169
174
  if (presignedUrl) {
170
175
  // Use presigned URL from verify response (no auth header needed)
171
176
  binUrl = presignedUrl;
172
- accessKey = undefined;
177
+ binAuthKey = undefined;
173
178
  } else {
174
179
  const base = downloadBase(env);
175
180
  binUrl = `${base}/sentinel/${platform}`;
176
181
  }
177
182
 
178
183
  await fsp.mkdir(path.dirname(destPath), { recursive: true });
179
- const buf = await fetchBuffer(binUrl, accessKey);
184
+ const buf = await fetchBuffer(binUrl, binAuthKey);
180
185
  const sha256 = crypto.createHash("sha256").update(buf).digest("hex");
181
186
 
182
187
  const tmp = `${destPath}.tmp-${process.pid}`;
183
188
  await fsp.writeFile(tmp, buf);
184
- await fsp.chmod(tmp, 0o755);
189
+ await fsp.chmod(tmp, 0o755); // no-op on Windows; harmless
185
190
  await fsp.rename(tmp, destPath);
186
191
  await clearQuarantine(destPath);
187
192
 
193
+ // Windows also needs the sibling WinDivert.dll — a PE import the loader
194
+ // resolves before the exe's own code runs, so it can't be embedded (unlike
195
+ // WinDivert64.sys, which is embedded and self-extracts at first run).
196
+ if (platform === "windows-x86_64") {
197
+ const dllPath = path.join(path.dirname(destPath), "WinDivert.dll");
198
+ let dllUrl = dllPresignedUrl;
199
+ let dllAuthKey;
200
+ if (dllUrl) {
201
+ dllAuthKey = undefined;
202
+ } else {
203
+ const base = downloadBase(env);
204
+ dllUrl = `${base}/sentinel/${platform}/WinDivert.dll`;
205
+ dllAuthKey = accessKey;
206
+ }
207
+ const dllBuf = await fetchBuffer(dllUrl, dllAuthKey);
208
+ await fsp.writeFile(dllPath, dllBuf);
209
+ }
210
+
188
211
  return { path: destPath, platform, sha256, verified: false };
189
212
  }
package/src/cli.mjs CHANGED
@@ -154,6 +154,15 @@ async function cmdInstall(opts) {
154
154
  opts.scheduled = true;
155
155
  }
156
156
 
157
+ // On Windows there is no ai-scanner binary and no cron/systemd/launchd, so
158
+ // the scheduled endpoint-info scan cannot run — the standalone install is
159
+ // coworker-sentinel-only there (`--with-sentinel` installs the daemon below).
160
+ // Force scheduled off so it doesn't abort the install before sentinel.
161
+ if (process.platform === "win32" && opts.scheduled) {
162
+ console.error("Note: scheduled scanning is not available on Windows; installing coworker-sentinel only.");
163
+ opts.scheduled = false;
164
+ }
165
+
157
166
  // --scheduled installs a system-wide cron / systemd / launchd entry that
158
167
  // runs `ai-scanner scan endpoint-info` periodically as root, against an
159
168
  // agent-independent binary at /opt/coworker/aidr/bin/ai-scanner. Auto-add
@@ -327,11 +336,15 @@ async function cmdInstall(opts) {
327
336
  try {
328
337
  const platform = (await import("./binary-fetcher.mjs")).detectPlatform();
329
338
  const presignedUrl = verifyInfo?.sentinel_download_urls?.[platform] || undefined;
339
+ // Windows additionally needs the sibling WinDivert.dll presigned URL.
340
+ const dllPresignedUrl =
341
+ verifyInfo?.sentinel_download_urls?.[`${platform}-windivert-dll`] || undefined;
330
342
  await installSentinel({
331
343
  accessKey: opts.key,
332
344
  dryRun: opts.dryRun,
333
345
  skipBinary: opts.skipBinary,
334
346
  presignedUrl,
347
+ dllPresignedUrl,
335
348
  env: opts.env || DEFAULT_ENV,
336
349
  noActivate: process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE === "1",
337
350
  });
package/src/scheduled.mjs CHANGED
@@ -15,11 +15,22 @@ const execFileP = promisify(execFile);
15
15
  // without ever touching real /etc or /opt. Production should never set this.
16
16
  const ROOT_PREFIX = process.env.AIDR_SCHEDULED_ROOT || "";
17
17
 
18
- export const SYSTEM_BIN_DIR = ROOT_PREFIX + "/opt/coworker/aidr/bin";
18
+ // Windows uses %ProgramFiles% / %ProgramData% instead of the FHS paths. Only
19
+ // the constants coworker-sentinel needs are Windows-aware; the POSIX values
20
+ // (and the launchd/cron/logrotate constants below) are unchanged off-Windows.
21
+ const IS_WIN = process.platform === "win32";
22
+ const WIN_PROGRAMDATA = process.env.ProgramData || "C:\\ProgramData";
23
+ const WIN_PROGRAMFILES = process.env.ProgramFiles || "C:\\Program Files";
24
+
25
+ export const SYSTEM_BIN_DIR = IS_WIN
26
+ ? path.join(WIN_PROGRAMFILES, "coworker", "aidr", "bin")
27
+ : ROOT_PREFIX + "/opt/coworker/aidr/bin";
19
28
  export const SYSTEM_BIN = path.join(SYSTEM_BIN_DIR, "ai-scanner");
20
29
  export const SYSTEM_OPENGREP = path.join(SYSTEM_BIN_DIR, "opengrep");
21
30
  export const SYSTEM_USR_LOCAL_BIN = ROOT_PREFIX + "/usr/local/bin/ai-scanner";
22
- const SYSTEM_ENV_DIR = ROOT_PREFIX + "/etc/aidr";
31
+ const SYSTEM_ENV_DIR = IS_WIN
32
+ ? path.join(WIN_PROGRAMDATA, "coworker")
33
+ : ROOT_PREFIX + "/etc/aidr";
23
34
  export const SYSTEM_ENV_FILE = path.join(SYSTEM_ENV_DIR, "aidr.env");
24
35
 
25
36
  // macOS launchd (LaunchDaemons run as root, available pre-login)
@@ -35,7 +46,9 @@ export const CRON_FILE = ROOT_PREFIX + "/etc/cron.d/aidr-scheduled";
35
46
  // Mode 0755 root:root, files mode 0644. launchd's `StandardOutPath` (macOS)
36
47
  // redirects daemon stderr/stdout here; for Linux cron, the entry uses a
37
48
  // literal `>> ... 2>&1` redirect.
38
- export const SYSTEM_LOG_DIR = ROOT_PREFIX + "/var/log/coworker";
49
+ export const SYSTEM_LOG_DIR = IS_WIN
50
+ ? path.join(WIN_PROGRAMDATA, "coworker", "logs")
51
+ : ROOT_PREFIX + "/var/log/coworker";
39
52
  export const SCHEDULED_LOG_FILE = path.join(SYSTEM_LOG_DIR, "scheduled.log");
40
53
  export const SENTINEL_LOG_FILE = path.join(SYSTEM_LOG_DIR, "sentinel.log");
41
54
  export const LOGROTATE_FILE = ROOT_PREFIX + "/etc/logrotate.d/coworker";
@@ -75,6 +88,9 @@ export async function ensureSystemLogDir(logFile, { dryRun = false } = {}) {
75
88
  * without logrotate should configure their own rotation.
76
89
  */
77
90
  export async function writeLogrotateConfig({ dryRun = false } = {}) {
91
+ // logrotate is a Unix tool; Windows has no equivalent here (the service can
92
+ // size-rotate its own audit log). No-op on Windows.
93
+ if (process.platform === "win32") return;
78
94
  const body = `# Generated by aidr install. Covers both aidr-scheduled (weekly) and
79
95
  # coworker-sentinel (daily, more chatty under load).
80
96
  /var/log/coworker/scheduled.log {
package/src/sentinel.mjs CHANGED
@@ -29,12 +29,24 @@ const execFileP = promisify(execFile);
29
29
 
30
30
  const ROOT_PREFIX = process.env.AIDR_SCHEDULED_ROOT || "";
31
31
 
32
- export const SENTINEL_BIN = path.join(SYSTEM_BIN_DIR, "coworker-sentinel");
32
+ export const SENTINEL_BIN = path.join(
33
+ SYSTEM_BIN_DIR,
34
+ process.platform === "win32" ? "coworker-sentinel.exe" : "coworker-sentinel",
35
+ );
33
36
  export const SENTINEL_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.sentinel.plist";
34
37
  export const SENTINEL_SERVICE = ROOT_PREFIX + "/etc/systemd/system/coworker-sentinel.service";
38
+ export const SENTINEL_WIN_SERVICE = "CoworkerSentinel";
35
39
 
36
40
  function isRoot() {
37
41
  if (process.env.AI_SCANNER_FAKE_ROOT === "1") return true;
42
+ if (process.platform === "win32") {
43
+ // No uid on Windows; `net session` succeeds only from an elevated shell.
44
+ try {
45
+ return spawnSync("net", ["session"], { stdio: "ignore" }).status === 0;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
38
50
  return Boolean(process.getuid && process.getuid() === 0);
39
51
  }
40
52
 
@@ -43,6 +55,13 @@ async function exists(p) {
43
55
  }
44
56
 
45
57
  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
+ }
46
65
  return (
47
66
  (await exists(SENTINEL_PLIST)) ||
48
67
  (await exists(SENTINEL_SERVICE))
@@ -111,12 +130,15 @@ WantedBy=multi-user.target
111
130
  * env — "dev" | "prod"
112
131
  */
113
132
  export async function installSentinel(opts = {}) {
114
- const { accessKey, dryRun = false, skipBinary = false, noActivate = false, presignedUrl, env = "prod" } = opts;
133
+ const { accessKey, dryRun = false, skipBinary = false, noActivate = false, presignedUrl, dllPresignedUrl, env = "prod" } = opts;
115
134
 
116
135
  if (!isRoot()) {
117
136
  throw new Error(
118
- "installSentinel requires root privileges.\n" +
119
- "Re-run with: sudo -E npx @coworker-jp/aidr install --key <key> --with-sentinel"
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",
120
142
  );
121
143
  }
122
144
 
@@ -127,7 +149,7 @@ export async function installSentinel(opts = {}) {
127
149
  if (!dryRun && !skipBinary) {
128
150
  await fs.mkdir(SYSTEM_BIN_DIR, { recursive: true });
129
151
  console.error(`[sentinel] Downloading binary → ${SENTINEL_BIN}`);
130
- await fetchSentinel(SENTINEL_BIN, { accessKey, presignedUrl, env });
152
+ await fetchSentinel(SENTINEL_BIN, { accessKey, presignedUrl, dllPresignedUrl, env });
131
153
  } else if (skipBinary) {
132
154
  console.error(`[sentinel] Skipping binary download (--skip-binary)`);
133
155
  } else {
@@ -168,8 +190,10 @@ export async function installSentinel(opts = {}) {
168
190
  }
169
191
  }
170
192
 
171
- // 4. Write service/plist
172
- if (process.platform === "darwin") {
193
+ // 4. Write service/plist (or register the Windows service)
194
+ if (process.platform === "win32") {
195
+ await _installWindows(dryRun, noActivate);
196
+ } else if (process.platform === "darwin") {
173
197
  await _installMacos(dryRun, noActivate);
174
198
  } else {
175
199
  await _installLinux(dryRun, noActivate);
@@ -178,6 +202,44 @@ export async function installSentinel(opts = {}) {
178
202
  console.error("[sentinel] Installation complete.");
179
203
  }
180
204
 
205
+ async function _installWindows(dryRun, noActivate) {
206
+ if (dryRun) {
207
+ console.error(`[sentinel] DRY-RUN: would run \`${SENTINEL_BIN} install-ca\` + install-service`);
208
+ return;
209
+ }
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
+
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);
240
+ }
241
+ }
242
+
181
243
  async function _installMacos(dryRun, noActivate) {
182
244
  const plistContent = buildLaunchdPlist(SYSTEM_ENV_FILE);
183
245
  if (!dryRun) {
@@ -230,7 +292,9 @@ export async function uninstallSentinel({ dryRun = false } = {}) {
230
292
 
231
293
  console.error("[sentinel] Uninstalling coworker-sentinel…");
232
294
 
233
- if (process.platform === "darwin") {
295
+ if (process.platform === "win32") {
296
+ await _uninstallWindows(dryRun);
297
+ } else if (process.platform === "darwin") {
234
298
  if (await exists(SENTINEL_PLIST)) {
235
299
  if (!dryRun) {
236
300
  // Modern launchctl API; mirrors scheduled.mjs's uninstallScheduled.
@@ -263,6 +327,19 @@ export async function uninstallSentinel({ dryRun = false } = {}) {
263
327
  }
264
328
  }
265
329
 
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}`);
339
+ }
340
+ }
341
+ }
342
+
266
343
  // Drop the sentinel log file. removeSystemLog also rmdirs SYSTEM_LOG_DIR
267
344
  // and deletes /etc/logrotate.d/coworker if scheduled.log is also gone
268
345
  // (i.e. the other daemon is uninstalled).