@coworker-jp/aidr 0.1.0 → 0.1.2

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.0",
3
+ "version": "0.1.2",
4
4
  "description": "AIDR setup CLI - installs ai-scanner hooks for 19+ AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -589,7 +589,7 @@ export async function run(argv) {
589
589
  program
590
590
  .name("aidr")
591
591
  .description("AIDR setup CLI — installs ai-scanner hooks for AI coding agents")
592
- .version("0.1.0");
592
+ .version("0.1.2");
593
593
 
594
594
  program
595
595
  .command("install")
package/src/scheduled.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
- import { execFile, spawnSync } from "child_process";
3
+ import { execFile } from "child_process";
4
4
  import { promisify } from "util";
5
5
 
6
6
  const execFileP = promisify(execFile);
@@ -177,82 +177,25 @@ export async function installScheduled(opts) {
177
177
  }
178
178
 
179
179
  async function writeEnvFile(accessKey, dryRun) {
180
- // Capture the install-time actor identity here (NOT at scheduled
181
- // run-time) because the scheduler runs as root with no Apple ID, no
182
- // git config, and $USER="root". Without this, the Rust scanner would
183
- // emit snapshots labelled "unknown" (or worse, fall through to
184
- // customer_email the bug this whole helper exists to fix).
185
- const actorId = resolveInstallerActorId();
180
+ // Capture only the SUDO_USER (the unix username of whoever ran
181
+ // `sudo aidr install ...`) the Rust binary will resolve the full
182
+ // identity (Apple ID / git email / username) at scan time using
183
+ // `sudo -u <invoker>` so it reads the invoker's plist / gitconfig
184
+ // instead of root's empty ones. Pre-resolving here is the wrong
185
+ // layer because the Apple ID needs to be re-read in the right user
186
+ // context at runtime anyway (root has no MobileMeAccounts.plist).
187
+ const invoker = process.env.SUDO_USER || null;
186
188
  let envContent = `AI_SCANNER_ACCESS_KEY=${accessKey}\n`;
187
- if (actorId) envContent += `AI_SCANNER_ACTOR_ID=${actorId}\n`;
189
+ if (invoker) envContent += `AI_SCANNER_INVOKER_USER=${invoker}\n`;
188
190
  if (dryRun) {
189
191
  console.log("[dry-run] Would create:", SYSTEM_ENV_FILE, "(mode 0600)");
190
- if (actorId) console.log("[dry-run] actor_id captured:", actorId);
192
+ if (invoker) console.log("[dry-run] invoker captured:", invoker);
191
193
  return;
192
194
  }
193
195
  await fs.mkdir(SYSTEM_ENV_DIR, { recursive: true, mode: 0o755 });
194
196
  await fs.writeFile(SYSTEM_ENV_FILE, envContent, { mode: 0o600 });
195
197
  }
196
198
 
197
- /**
198
- * Resolve the **pre-sudo user's** identity for embedding in
199
- * AI_SCANNER_ACTOR_ID at install time. Order, per user spec:
200
- * 1. SUDO_USER's git config user.email (run as that user — root has
201
- * its own (usually empty) git config that we must not return)
202
- * 2. macOS: SUDO_USER's Apple ID (`defaults read MobileMeAccounts`)
203
- * 3. SUDO_USER itself (the Unix username)
204
- *
205
- * Returns `null` when none of the above succeed (e.g. install was run
206
- * as bare root with no SUDO_USER). The caller writes nothing in that
207
- * case; the Rust scanner then surfaces "unknown" instead of mislabeling
208
- * the snapshot with the user-portal login email.
209
- *
210
- * Test override: AIDR_INSTALLER_ACTOR_ID env bypasses every detection
211
- * step (used by Node unit tests so they don't have to mock spawnSync).
212
- */
213
- export function resolveInstallerActorId() {
214
- if (process.env.AIDR_INSTALLER_ACTOR_ID) {
215
- return process.env.AIDR_INSTALLER_ACTOR_ID || null;
216
- }
217
- const sudoUser = process.env.SUDO_USER;
218
- if (!sudoUser) return null;
219
-
220
- // 1. git config user.email — run AS the SUDO_USER so we read their
221
- // ~/.gitconfig, not /root/.gitconfig (which is normally empty).
222
- // --global is intentional: per-repo configs aren't relevant for
223
- // a host-level identity. timeout caps a hung git invocation.
224
- const gitRes = spawnSync(
225
- "sudo",
226
- ["-u", sudoUser, "git", "config", "--global", "--get", "user.email"],
227
- { encoding: "utf8", timeout: 5000 },
228
- );
229
- if (gitRes.status === 0) {
230
- const email = (gitRes.stdout || "").trim();
231
- if (email) return email;
232
- }
233
-
234
- // 2. macOS Apple ID. Mirrors the Rust binary's parse_apple_id: read
235
- // the first AccountID line from MobileMeAccounts. Must run as the
236
- // SUDO_USER — root has no MobileMeAccounts plist of its own.
237
- if (process.platform === "darwin") {
238
- const dRes = spawnSync(
239
- "sudo",
240
- ["-u", sudoUser, "defaults", "read", "MobileMeAccounts"],
241
- { encoding: "utf8", timeout: 5000 },
242
- );
243
- if (dRes.status === 0) {
244
- const m = (dRes.stdout || "").match(/AccountID\s*=\s*"([^"]+)"/);
245
- const id = m && m[1] ? m[1].trim() : "";
246
- if (id) return id;
247
- }
248
- }
249
-
250
- // 3. Username fallback. Better than "unknown" because at least it
251
- // ties the snapshot to a Unix account on the host, but it loses
252
- // the cross-host identity that an email gives us.
253
- return sudoUser;
254
- }
255
-
256
199
  async function installSystemd({ binaryPath, dryRun, noActivate, interval = 4 }) {
257
200
  // StandardOutput=append: requires systemd 240+ (released 2018). Modern
258
201
  // distros (RHEL 9 / Ubuntu 20.04+ / Debian 11+) all support it. Older
package/src/sentinel.mjs CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  ensureSystemLogDir,
23
23
  writeLogrotateConfig,
24
24
  removeSystemLog,
25
- resolveInstallerActorId,
26
25
  } from "./scheduled.mjs";
27
26
 
28
27
  const execFileP = promisify(execFile);
@@ -139,14 +138,15 @@ export async function installSentinel(opts = {}) {
139
138
  await ensureSystemLogDir(SENTINEL_LOG_FILE, { dryRun });
140
139
  await writeLogrotateConfig({ dryRun });
141
140
 
142
- // 3. Write env file (append AI_SCANNER_ACCESS_KEY + AI_SCANNER_ACTOR_ID
141
+ // 3. Write env file (append AI_SCANNER_ACCESS_KEY + AI_SCANNER_INVOKER_USER
143
142
  // if missing). When --with-sentinel runs alongside --scheduled,
144
143
  // scheduled.mjs's writeEnvFile has already written both lines and
145
144
  // we no-op. When --with-sentinel runs standalone (no --scheduled),
146
- // we are the only writer of aidr.env and must capture the
147
- // pre-sudo SUDO_USER identity ourselves; otherwise the daemon
148
- // would report `unknown` actor (or worse, the customer_email
149
- // fallback before that branch was removed).
145
+ // we are the only writer of aidr.env and must capture SUDO_USER
146
+ // ourselves; otherwise the daemon (running as root with no useful
147
+ // git/Apple/USER) reports `unknown`. Identity resolution itself
148
+ // (Apple ID / git email / username) happens at scan time inside
149
+ // the Rust binary using `sudo -u <invoker>`.
150
150
  if (accessKey && !dryRun) {
151
151
  let content = "";
152
152
  try { content = await fs.readFile(SYSTEM_ENV_FILE, "utf8"); } catch { /* first run */ }
@@ -154,10 +154,10 @@ export async function installSentinel(opts = {}) {
154
154
  if (!content.includes("AI_SCANNER_ACCESS_KEY")) {
155
155
  appended += `\nAI_SCANNER_ACCESS_KEY=${accessKey}\n`;
156
156
  }
157
- if (!content.includes("AI_SCANNER_ACTOR_ID")) {
158
- const actorId = resolveInstallerActorId();
159
- if (actorId) {
160
- appended += `AI_SCANNER_ACTOR_ID=${actorId}\n`;
157
+ if (!content.includes("AI_SCANNER_INVOKER_USER")) {
158
+ const invoker = process.env.SUDO_USER;
159
+ if (invoker) {
160
+ appended += `AI_SCANNER_INVOKER_USER=${invoker}\n`;
161
161
  }
162
162
  }
163
163
  if (appended) {