@coworker-jp/aidr 0.1.318 → 0.1.321
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 +1 -1
- package/src/agents/claude.mjs +4 -1
- package/src/agents/codex.mjs +4 -1
- package/src/agents/cursor.mjs +4 -1
- package/src/agents/gemini.mjs +8 -3
- package/src/agents/kiro.mjs +4 -1
- package/src/agents/standalone.mjs +4 -1
- package/src/agents/windsurf.mjs +4 -1
- package/src/binary-fetcher.mjs +26 -7
- package/src/cli.mjs +47 -15
- package/src/install-status.mjs +4 -1
- package/src/merge.mjs +32 -6
- package/src/templates.mjs +133 -52
package/package.json
CHANGED
package/src/agents/claude.mjs
CHANGED
|
@@ -21,7 +21,10 @@ const SCRIPT_FILES = [
|
|
|
21
21
|
// `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
|
|
22
22
|
// 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
|
|
23
23
|
// 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
|
|
24
|
-
|
|
24
|
+
// #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
|
|
25
|
+
// `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
|
|
26
|
+
// 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
|
|
27
|
+
const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
|
|
25
28
|
|
|
26
29
|
export const meta = {
|
|
27
30
|
name: "claude",
|
package/src/agents/codex.mjs
CHANGED
|
@@ -35,7 +35,10 @@ const SCRIPT_FILES = [
|
|
|
35
35
|
// `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
|
|
36
36
|
// 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
|
|
37
37
|
// 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
|
|
38
|
-
|
|
38
|
+
// #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
|
|
39
|
+
// `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
|
|
40
|
+
// 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
|
|
41
|
+
const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
|
|
39
42
|
const ROOT_FILES = ["AGENTS.md"];
|
|
40
43
|
|
|
41
44
|
export const meta = {
|
package/src/agents/cursor.mjs
CHANGED
|
@@ -36,7 +36,10 @@ const SCRIPT_FILES = [
|
|
|
36
36
|
// `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
|
|
37
37
|
// 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
|
|
38
38
|
// 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
|
|
39
|
-
|
|
39
|
+
// #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
|
|
40
|
+
// `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
|
|
41
|
+
// 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
|
|
42
|
+
const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
|
|
40
43
|
|
|
41
44
|
export const meta = {
|
|
42
45
|
name: "cursor",
|
package/src/agents/gemini.mjs
CHANGED
|
@@ -13,7 +13,10 @@ const HOOK_FILES = ["aidr-pre.sh"];
|
|
|
13
13
|
// `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
|
|
14
14
|
// 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
|
|
15
15
|
// 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
|
|
16
|
-
|
|
16
|
+
// #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
|
|
17
|
+
// `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
|
|
18
|
+
// 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
|
|
19
|
+
const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
|
|
17
20
|
|
|
18
21
|
export const meta = {
|
|
19
22
|
name: "gemini",
|
|
@@ -26,11 +29,13 @@ export async function install(home, accessKey, opts = {}) {
|
|
|
26
29
|
const { dryRun = false, scope = "project" } = opts;
|
|
27
30
|
const base = meta.configDir(home);
|
|
28
31
|
const hooks = path.join(base, "hooks");
|
|
29
|
-
|
|
32
|
+
// #2366: hook が呼ぶのは `ai-harness` (8 コンテキスト)。`ai-scanner` は
|
|
33
|
+
// 定期インベントリ送信で、`scan read-input` 等のサブコマンドを持たない。
|
|
34
|
+
const binary = path.join(home, meta.agentDir, "bin", "ai-harness");
|
|
30
35
|
const settingsPath = path.join(base, "settings.json");
|
|
31
36
|
const preHookPath = path.join(hooks, "aidr-pre.sh");
|
|
32
37
|
const [settingsRes, hookRes] = await Promise.all([
|
|
33
|
-
writeJsonMerge(settingsPath, buildGeminiSettings(accessKey,
|
|
38
|
+
writeJsonMerge(settingsPath, buildGeminiSettings(accessKey, hooks), mergeGeminiSettings, { dryRun, backup: scope !== "project" }),
|
|
34
39
|
writeFileSafe(preHookPath, T.getGeminiPreHookSh(binary), { dryRun, force: true, mode: 0o755 }),
|
|
35
40
|
]);
|
|
36
41
|
return {
|
package/src/agents/kiro.mjs
CHANGED
|
@@ -26,7 +26,10 @@ const SCRIPT_FILES = [
|
|
|
26
26
|
// `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
|
|
27
27
|
// 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
|
|
28
28
|
// 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
|
|
29
|
-
|
|
29
|
+
// #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
|
|
30
|
+
// `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
|
|
31
|
+
// 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
|
|
32
|
+
const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
|
|
30
33
|
|
|
31
34
|
export const meta = {
|
|
32
35
|
name: "kiro",
|
|
@@ -10,7 +10,10 @@ import { pathExists, rmdirIfEmpty } from "../fs-utils.mjs";
|
|
|
10
10
|
// `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
|
|
11
11
|
// 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
|
|
12
12
|
// 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
|
|
13
|
-
|
|
13
|
+
// #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
|
|
14
|
+
// `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
|
|
15
|
+
// 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
|
|
16
|
+
const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
|
|
14
17
|
|
|
15
18
|
export const meta = {
|
|
16
19
|
name: "standalone",
|
package/src/agents/windsurf.mjs
CHANGED
|
@@ -33,7 +33,10 @@ const SCRIPT_FILES = [
|
|
|
33
33
|
// `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
|
|
34
34
|
// 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
|
|
35
35
|
// 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
|
|
36
|
-
|
|
36
|
+
// #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
|
|
37
|
+
// `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
|
|
38
|
+
// 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
|
|
39
|
+
const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
|
|
37
40
|
|
|
38
41
|
export const meta = {
|
|
39
42
|
name: "windsurf",
|
package/src/binary-fetcher.mjs
CHANGED
|
@@ -282,8 +282,18 @@ export async function fetchAsset(binUrl, destPath, { accessKey, checksumOpts } =
|
|
|
282
282
|
return { path: destPath, sha256, verified };
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
//: The product served at the BARE platform key. Every other product is
|
|
286
|
+
//: addressed as `<product>/<platform>`; see `fetchBinary` for why this one is
|
|
287
|
+
//: not, and why that asymmetry is deliberate rather than an oversight.
|
|
288
|
+
export const DEFAULT_PRODUCT = "ai-scanner";
|
|
289
|
+
|
|
290
|
+
//: The hook binary (issue #2348 split, wired up in #2366). This is what every
|
|
291
|
+
//: hook adapter invokes — the eight tool-call scan contexts live here, NOT in
|
|
292
|
+
//: `ai-scanner`, which is the periodic endpoint-inventory collector.
|
|
293
|
+
export const HARNESS_PRODUCT = "ai-harness";
|
|
294
|
+
|
|
285
295
|
/**
|
|
286
|
-
* Download
|
|
296
|
+
* Download one scanner-family binary for the current platform into `destPath`.
|
|
287
297
|
*
|
|
288
298
|
* TWO KEYS, ONE FALLBACK (issue #1687)
|
|
289
299
|
* ------------------------------------
|
|
@@ -312,19 +322,28 @@ export async function fetchAsset(binUrl, destPath, { accessKey, checksumOpts } =
|
|
|
312
322
|
* `servedUnpinned: true` means the caller must NOT claim the pinned version was
|
|
313
323
|
* installed — what landed is whatever the endpoint currently publishes.
|
|
314
324
|
*/
|
|
315
|
-
export async function fetchBinary(destPath, { env = "prod", accessKey, version, baseUrl } = {}) {
|
|
325
|
+
export async function fetchBinary(destPath, { env = "prod", accessKey, version, baseUrl, product = DEFAULT_PRODUCT } = {}) {
|
|
316
326
|
const platform = detectPlatform();
|
|
317
327
|
const base = baseUrl || downloadBase(env);
|
|
318
|
-
|
|
328
|
+
// ONE ROUTE, TWO PRODUCTS (issue #2366)
|
|
329
|
+
// ------------------------------------
|
|
330
|
+
// `ai-scanner` keeps the BARE platform key it has always had — that key is a
|
|
331
|
+
// wire contract with every endpoint already in the field, and the binary's own
|
|
332
|
+
// self-updater composes it. Everything else is addressed under its own name.
|
|
333
|
+
// Composing `ai-scanner/<platform>` "for symmetry" would ask a Lambda that
|
|
334
|
+
// predates the split for a path it rejects, and the endpoints that would hit
|
|
335
|
+
// it are exactly the ones with no other way in.
|
|
336
|
+
const seg = product === DEFAULT_PRODUCT ? "" : `${product}/`;
|
|
337
|
+
const unpinnedUrl = `${base}/${seg}${platform}`;
|
|
319
338
|
|
|
320
339
|
if (!version) {
|
|
321
340
|
const result = await fetchAsset(unpinnedUrl, destPath, { accessKey });
|
|
322
|
-
return { ...result, platform, requestedVersion: null, servedUnpinned: false };
|
|
341
|
+
return { ...result, platform, product, requestedVersion: null, servedUnpinned: false };
|
|
323
342
|
}
|
|
324
343
|
|
|
325
344
|
try {
|
|
326
|
-
const result = await fetchAsset(`${base}/v${version}/${platform}`, destPath, { accessKey });
|
|
327
|
-
return { ...result, platform, requestedVersion: version, servedUnpinned: false };
|
|
345
|
+
const result = await fetchAsset(`${base}/v${version}/${seg}${platform}`, destPath, { accessKey });
|
|
346
|
+
return { ...result, platform, product, requestedVersion: version, servedUnpinned: false };
|
|
328
347
|
} catch (e) {
|
|
329
348
|
// A pinned release whose CHECKSUM is a definite 404 is incomplete in exactly
|
|
330
349
|
// the same way as one whose binary is (#1773): half a release is not a
|
|
@@ -333,7 +352,7 @@ export async function fetchBinary(destPath, { env = "prod", accessKey, version,
|
|
|
333
352
|
// unverified install, because `fetchAsset` refuses that on both keys.
|
|
334
353
|
if (e?.status !== 404) throw e;
|
|
335
354
|
const result = await fetchAsset(unpinnedUrl, destPath, { accessKey });
|
|
336
|
-
return { ...result, platform, requestedVersion: version, servedUnpinned: true };
|
|
355
|
+
return { ...result, platform, product, requestedVersion: version, servedUnpinned: true };
|
|
337
356
|
}
|
|
338
357
|
}
|
|
339
358
|
|
package/src/cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { Command } from "commander";
|
|
|
10
10
|
const execFileP = promisify(execFile);
|
|
11
11
|
import { AGENT_ORDER, AGENT_REGISTRY, loadAgent } from "./agents/index.mjs";
|
|
12
12
|
import { verifyKey, maskKey } from "./verify.mjs";
|
|
13
|
-
import { fetchBinary, fetchOpengrep, assertVerified } from "./binary-fetcher.mjs";
|
|
13
|
+
import { fetchBinary, fetchOpengrep, assertVerified, HARNESS_PRODUCT } from "./binary-fetcher.mjs";
|
|
14
14
|
import { detectInstalled } from "./detect.mjs";
|
|
15
15
|
import { evaluateInstallGate, formatLocalPcGuidance } from "./env-gate.mjs";
|
|
16
16
|
import { resolveCallerHome, isRunningUnderSudo, chownToCaller } from "./sudo-user.mjs";
|
|
@@ -201,15 +201,33 @@ async function cmdInstall(opts) {
|
|
|
201
201
|
version: PINNED_VERSION,
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
204
|
+
// THREE files, one bin dir (issue #2366).
|
|
205
|
+
//
|
|
206
|
+
// ai-harness the hook binary — the eight tool-call scan contexts.
|
|
207
|
+
// Every adapter this installer writes invokes THIS one,
|
|
208
|
+
// so it is fetched first: it is what "this machine is
|
|
209
|
+
// being scanned" means.
|
|
210
|
+
// ai-scanner the periodic endpoint-inventory collector, and the
|
|
211
|
+
// self-update vehicle start_scanner.sh drives.
|
|
212
|
+
// opengrep bundled Opengrep (LGPL-2.1), a sibling so the scanner's
|
|
213
|
+
// sibling-lookup finds it without relying on PATH. Users
|
|
214
|
+
// can point AI_SCANNER_OPENGREP elsewhere to substitute
|
|
215
|
+
// their own LGPL-2.1 build.
|
|
216
|
+
const harnessRes = assertVerified(
|
|
217
|
+
await fetchBinary(path.join(primaryDir, "ai-harness"),
|
|
218
|
+
{ ...fetchOpts, product: HARNESS_PRODUCT }),
|
|
219
|
+
"the ai-harness binary",
|
|
220
|
+
);
|
|
221
|
+
// Set by the HOOK binary, not the periodic one: this flag is what
|
|
222
|
+
// decides between "nothing on this machine scans" and "scanning is in
|
|
223
|
+
// place, one piece is missing". Tying it to ai-scanner would report the
|
|
224
|
+
// reassuring half of that pair while every tool call ran unscanned.
|
|
225
|
+
binaryOutcome.scannerInstalled = true;
|
|
226
|
+
console.log(`binary: ${harnessRes.path} (${harnessRes.platform}, sha256 verified)`);
|
|
208
227
|
const scannerRes = assertVerified(
|
|
209
228
|
await fetchBinary(path.join(primaryDir, "ai-scanner"), fetchOpts),
|
|
210
229
|
"the ai-scanner binary",
|
|
211
230
|
);
|
|
212
|
-
binaryOutcome.scannerInstalled = true;
|
|
213
231
|
// Stated unconditionally, because it is now a condition of getting here
|
|
214
232
|
// rather than a caption on a file that may or may not have been checked
|
|
215
233
|
// (#1773): `fetchAsset` throws unless the bytes matched the published
|
|
@@ -231,7 +249,7 @@ async function cmdInstall(opts) {
|
|
|
231
249
|
console.log(`binary: ${opengrepRes.path} (opengrep ${opengrepRes.platform}, sha256 verified)`);
|
|
232
250
|
|
|
233
251
|
const copyOps = restDirs.flatMap(restDir =>
|
|
234
|
-
["ai-scanner", "opengrep"].map(async (file) => {
|
|
252
|
+
["ai-harness", "ai-scanner", "opengrep"].map(async (file) => {
|
|
235
253
|
const src = path.join(primaryDir, file);
|
|
236
254
|
const dest = path.join(restDir, file);
|
|
237
255
|
await fs.mkdir(restDir, { recursive: true });
|
|
@@ -366,7 +384,10 @@ async function cmdInstall(opts) {
|
|
|
366
384
|
const stranded = needBinary.filter((r) => !r.selfHeals).map((r) => r.displayName);
|
|
367
385
|
const binPaths = (await Promise.all(needBinary.map(async (r) => {
|
|
368
386
|
const mod = await loadAgent(r.name);
|
|
369
|
-
|
|
387
|
+
// The hook binary: this list is read out to someone checking whether the
|
|
388
|
+
// machine is scanned, and `ai-scanner` sitting there would answer "yes"
|
|
389
|
+
// to a question about a file that runs no hooks (#2366).
|
|
390
|
+
return mod.meta.agentDir ? path.join(home, mod.meta.agentDir, "bin", "ai-harness") : null;
|
|
370
391
|
}))).filter(Boolean);
|
|
371
392
|
console.error(formatMissingBinaryStatus({
|
|
372
393
|
reason, selfHealing, stranded, binPaths, installCommand: INSTALL_COMMAND_HINT,
|
|
@@ -599,13 +620,24 @@ async function cmdDoctor() {
|
|
|
599
620
|
const mod = await loadAgent(name);
|
|
600
621
|
const agentDir = mod.meta.agentDir;
|
|
601
622
|
if (!agentDir) return null;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const
|
|
623
|
+
// TWO binaries since the split (issue #2366), and only ONE of them decides
|
|
624
|
+
// whether tool calls are scanned. `ai-harness` carries the eight hook
|
|
625
|
+
// contexts; `ai-scanner` is the periodic collector. Reporting only the
|
|
626
|
+
// latter would print "[present]" on a machine whose every hook fail-opens.
|
|
627
|
+
const bin = path.join(home, agentDir, "bin", "ai-harness");
|
|
628
|
+
const periodicBin = path.join(home, agentDir, "bin", "ai-scanner");
|
|
629
|
+
const exists = async (p) => {
|
|
630
|
+
try { await fs.access(p, fs.constants.F_OK); return true; } catch { return false; }
|
|
631
|
+
};
|
|
632
|
+
const present = await exists(bin);
|
|
633
|
+
const periodicPresent = await exists(periodicBin);
|
|
634
|
+
const out = [
|
|
635
|
+
`tool-call scanner: ${bin} ${present ? "[present]" : "[missing]"}`,
|
|
636
|
+
`periodic collector: ${periodicBin} ${periodicPresent ? "[present]" : "[missing]"}`,
|
|
637
|
+
];
|
|
638
|
+
if (!present) {
|
|
639
|
+
out.push(` Tool calls on this machine are NOT being scanned. Re-run the installer (${INSTALL_COMMAND_HINT}), or contact your IT administrator.`);
|
|
640
|
+
}
|
|
609
641
|
// What the hooks recorded while there was no binary (#1774). Without this,
|
|
610
642
|
// "[missing]" was the only trace, and it said nothing about how long the
|
|
611
643
|
// machine had been unscanned or how much had gone through unscanned —
|
package/src/install-status.mjs
CHANGED
|
@@ -41,7 +41,10 @@ export function formatMissingBinaryStatus({
|
|
|
41
41
|
const lines = [
|
|
42
42
|
"",
|
|
43
43
|
RULE,
|
|
44
|
-
|
|
44
|
+
// Names `ai-harness`, because that is the binary that scans a tool call
|
|
45
|
+
// (issue #2366). Saying "ai-scanner" here would send the reader to check a
|
|
46
|
+
// file that can be perfectly present on a machine where nothing is scanned.
|
|
47
|
+
"This machine is NOT being scanned yet — the ai-harness binary is not in place.",
|
|
45
48
|
"",
|
|
46
49
|
` what stopped it: ${reason}`,
|
|
47
50
|
"",
|
package/src/merge.mjs
CHANGED
|
@@ -24,8 +24,17 @@
|
|
|
24
24
|
export const CLAUDE_OUR_COMMAND_RE =
|
|
25
25
|
/^(?:"\$(?:CLAUDE_PROJECT_DIR|HOME)"|\$(?:CLAUDE_PROJECT_DIR|HOME))\/\.claude\/coworker-ai\/(?:start_scanner|stop_scanner|scan_[a-z_]+|log_web_urls)\.sh(?:\s|$|"|')/;
|
|
26
26
|
|
|
27
|
+
// Matches BOTH the adapter we install today AND the broken pre-#2354 command.
|
|
28
|
+
// **The legacy alternative is load-bearing, not tidiness**: an endpoint that
|
|
29
|
+
// already ran the old installer has the broken command sitting in its
|
|
30
|
+
// `settings.json`. `mergeGeminiSettings` / `unmergeGeminiSettings` use this
|
|
31
|
+
// predicate to recognise our own entries — if it stopped matching the old form,
|
|
32
|
+
// an upgrade would **leave the broken hook in place and add the new one beside
|
|
33
|
+
// it**, so every tool call would still hit a command that exits 2, and
|
|
34
|
+
// uninstall would leave it behind forever. Drop the legacy branch only once no
|
|
35
|
+
// endpoint in the field can still carry it.
|
|
27
36
|
export const GEMINI_OUR_COMMAND_RE =
|
|
28
|
-
/ai-scanner["']?\s+scan\s+hook-gemini\b/;
|
|
37
|
+
/(?:\/\.gemini\/hooks\/aidr-(?:pre|post)\.sh|ai-scanner["']?\s+scan\s+hook-gemini\b)/;
|
|
29
38
|
|
|
30
39
|
// Matches our Codex hook wrappers whether installed at the repo root
|
|
31
40
|
// (.codex/coworker-ai/pre_shell.sh) or absolute user-scope path
|
|
@@ -229,7 +238,27 @@ export function buildWindsurfHooksJson(scope = "project", absHome = "") {
|
|
|
229
238
|
};
|
|
230
239
|
}
|
|
231
240
|
|
|
232
|
-
|
|
241
|
+
// **2026-09-08 (issue #2354): this pointed at a subcommand that does not exist.**
|
|
242
|
+
// It emitted `<binary> scan hook-gemini --stage pre|post`; the scanner has no
|
|
243
|
+
// `hook-gemini` subcommand, so every invocation died with
|
|
244
|
+
// `error: unrecognized subcommand` and **exit code 2**. Install reported
|
|
245
|
+
// success, `settings.json` filled with hooks, and **not one tool call was ever
|
|
246
|
+
// scanned** — while the working adapter (`~/.gemini/hooks/aidr-pre.sh`) was
|
|
247
|
+
// written to disk on every install and referenced by nothing.
|
|
248
|
+
//
|
|
249
|
+
// Two things were wrong, not one. Exit 2 is also the *block* code, and this was
|
|
250
|
+
// the **only** agent calling the binary directly rather than through a shell
|
|
251
|
+
// wrapper — so it sat outside the fail-open invariant that every other agent
|
|
252
|
+
// gets from `run_scanner` (which maps a timeout's 124 to 0). Pointing at the
|
|
253
|
+
// adapter fixes both at once: the scan actually runs, **and** it runs inside
|
|
254
|
+
// the wrapper.
|
|
255
|
+
//
|
|
256
|
+
// `AfterTool` is gone rather than re-pointed: **there is no post adapter.**
|
|
257
|
+
// `agents/gemini.mjs` installs `aidr-pre.sh` only, and inventing post coverage
|
|
258
|
+
// here would be new agent coverage (#2350's scope), not a defect fix. Declaring
|
|
259
|
+
// only what exists is the point — the previous declaration is exactly how a
|
|
260
|
+
// hook that never ran looked installed.
|
|
261
|
+
export function buildGeminiSettings(accessKey, hooksDir) {
|
|
233
262
|
// timeout is in milliseconds (Gemini convention). Default 60000 (1 min)
|
|
234
263
|
// is too long for an inline hook — cap at 15000 so the editor kills a
|
|
235
264
|
// hung scanner cleanly. The shell-side run_scanner cap fires first under
|
|
@@ -241,10 +270,7 @@ export function buildGeminiSettings(accessKey, binary) {
|
|
|
241
270
|
},
|
|
242
271
|
hooks: {
|
|
243
272
|
BeforeTool: [
|
|
244
|
-
{ hooks: [{ type: "command", command: `"${
|
|
245
|
-
],
|
|
246
|
-
AfterTool: [
|
|
247
|
-
{ hooks: [{ type: "command", command: `"${binary}" scan hook-gemini --stage post`, timeout: 15000 }] },
|
|
273
|
+
{ hooks: [{ type: "command", command: `"${hooksDir}/aidr-pre.sh"`, timeout: 15000 }] },
|
|
248
274
|
],
|
|
249
275
|
},
|
|
250
276
|
};
|
package/src/templates.mjs
CHANGED
|
@@ -32,6 +32,16 @@ export function getStartScannerSh(downloadBase = "https://pytd2jd5p7.execute-api
|
|
|
32
32
|
# - Fail-open: never block Claude Code startup
|
|
33
33
|
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
34
34
|
BIN_DIR="$(dirname "$BIN")"
|
|
35
|
+
# TWO binaries since the split (issue #2366). \`ai-harness\` runs the eight
|
|
36
|
+
# tool-call scan contexts — it is the one every hook adapter invokes, so it is
|
|
37
|
+
# the one whose absence means "this session is not being scanned". \`ai-scanner\`
|
|
38
|
+
# is the periodic inventory collector and the self-update vehicle. They are
|
|
39
|
+
# built from one crate and therefore must stay on ONE version; the skew check
|
|
40
|
+
# further down is what enforces that.
|
|
41
|
+
#
|
|
42
|
+
# Two env names, never one shared: pointing a single override at one product
|
|
43
|
+
# would silently leave the other pointing at a file that is not it.
|
|
44
|
+
HARNESS="\${AI_HARNESS_BIN:-$BIN_DIR/ai-harness}"
|
|
35
45
|
# The self-update throttle belongs to the binary it throttles, so it is derived
|
|
36
46
|
# from \$BIN — the one source this script already trusts for "where is my copy"
|
|
37
47
|
# (\`.ai-scanner-unscanned\` is placed the same way). It used to be pinned to
|
|
@@ -132,7 +142,8 @@ sha256_of() {
|
|
|
132
142
|
# \`$UNVERIFIED_REASON\` carries WHY, so the message can name it.
|
|
133
143
|
download_binary() {
|
|
134
144
|
local url="$1"
|
|
135
|
-
local
|
|
145
|
+
local dest="$2"
|
|
146
|
+
local tmp=$(mktemp "\${BIN_DIR}/.download.XXXXXX")
|
|
136
147
|
local auth_args=()
|
|
137
148
|
if [ -n "\${AI_SCANNER_ACCESS_KEY:-}" ]; then
|
|
138
149
|
auth_args=(-H "x-access-key: \${AI_SCANNER_ACCESS_KEY}")
|
|
@@ -166,10 +177,33 @@ download_binary() {
|
|
|
166
177
|
# macOS: strip Gatekeeper quarantine so unsigned binary can execute
|
|
167
178
|
# (no-op on Linux where xattr isn't typically present)
|
|
168
179
|
xattr -d com.apple.quarantine "$tmp" 2>/dev/null || true
|
|
169
|
-
mv -f "$tmp" "$
|
|
180
|
+
mv -f "$tmp" "$dest"
|
|
170
181
|
return 0
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
# Fetch one product into place if it is not already there. The download route
|
|
185
|
+
# serves \`ai-scanner\` at the bare platform key (unchanged wire contract) and
|
|
186
|
+
# every other product under its own name.
|
|
187
|
+
#
|
|
188
|
+
# Says "installed" only when it actually installed something. Printing it on
|
|
189
|
+
# every session start for a file that was already there turns the line into
|
|
190
|
+
# noise, and the next reader stops distinguishing it from the one that matters.
|
|
191
|
+
# $1 = absolute destination, $2 = product name
|
|
192
|
+
ensure_binary() {
|
|
193
|
+
local dest="$1" product="$2" url
|
|
194
|
+
[ -x "$dest" ] && return 0
|
|
195
|
+
if [ "$product" = "ai-scanner" ]; then
|
|
196
|
+
url="\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}"
|
|
197
|
+
else
|
|
198
|
+
url="\${DOWNLOAD_BASE}/\${product}/\${PLATFORM_SLUG}"
|
|
199
|
+
fi
|
|
200
|
+
if download_binary "$url" "$dest"; then
|
|
201
|
+
echo "$product binary installed (\${PLATFORM_SLUG}, checksum verified)" >&2
|
|
202
|
+
return 0
|
|
203
|
+
fi
|
|
204
|
+
return 1
|
|
205
|
+
}
|
|
206
|
+
|
|
173
207
|
# What has already run unscanned on this machine, if anything (#1774). The scan
|
|
174
208
|
# hooks append to this file every time they exit without a binary; this is the
|
|
175
209
|
# first moment anyone can say it out loud, and it is deleted the moment a binary
|
|
@@ -234,26 +268,42 @@ case "$OS" in
|
|
|
234
268
|
;;
|
|
235
269
|
esac
|
|
236
270
|
|
|
237
|
-
#
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
271
|
+
# The HOOK binary first: tool-call scanning depends on this one, so its absence
|
|
272
|
+
# is what the session-level message is about.
|
|
273
|
+
#
|
|
274
|
+
# docs/product/UI_COPY.md 【非交渉】: the fact, stated so it cannot be read as
|
|
275
|
+
# harmless, and then the next action. The old single line ("Warning: Failed to
|
|
276
|
+
# download …") said neither what it meant for the session nor what to do, and it
|
|
277
|
+
# was the ONLY trace anywhere (#1774).
|
|
278
|
+
SCANNING=0
|
|
279
|
+
if ensure_binary "$HARNESS" ai-harness; then
|
|
280
|
+
SCANNING=1
|
|
281
|
+
# A binary IS in place. Clear any record of having run unscanned, so the
|
|
282
|
+
# record only ever describes the machine's CURRENT state.
|
|
283
|
+
rm -f "$UNSCANNED_STAMP"
|
|
284
|
+
else
|
|
285
|
+
echo "ai-scanner: this session is NOT being scanned — the tool-call scanner (ai-harness) is not installed on this machine and could not be installed now: \${UNVERIFIED_REASON:-the download failed}." >&2
|
|
286
|
+
report_unscanned_history
|
|
287
|
+
echo "ai-scanner: tool calls keep working, and they run unscanned until it is in place. Re-run the installer (${installCommand}), or contact your IT administrator with this message." >&2
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
# The PERIODIC binary. Its absence does NOT stop tool-call scanning, so it gets
|
|
291
|
+
# its own, narrower sentence rather than borrowing the one above — saying "this
|
|
292
|
+
# session is NOT being scanned" when it IS would train the reader to ignore the
|
|
293
|
+
# line that means it.
|
|
294
|
+
if ! ensure_binary "$BIN" ai-scanner; then
|
|
295
|
+
# Only claims scanning is unaffected when it actually is — the harness may
|
|
296
|
+
# have failed too, and the line above is the one that says so.
|
|
297
|
+
if [ "$SCANNING" = 1 ]; then
|
|
298
|
+
echo "ai-scanner: tool calls are still being scanned, but the periodic endpoint-inventory collector could not be installed: \${UNVERIFIED_REASON:-the download failed}. Re-run the installer (${installCommand}) if this endpoint stops appearing in the portal." >&2
|
|
242
299
|
else
|
|
243
|
-
|
|
244
|
-
# read as harmless, and then the next action. The old single line
|
|
245
|
-
# ("Warning: Failed to download …") said neither what it meant for the
|
|
246
|
-
# session nor what to do, and it was the ONLY trace anywhere (#1774).
|
|
247
|
-
echo "ai-scanner: this session is NOT being scanned — the scanner is not installed on this machine and could not be installed now: \${UNVERIFIED_REASON:-the download failed}." >&2
|
|
248
|
-
report_unscanned_history
|
|
249
|
-
echo "ai-scanner: tool calls keep working, and they run unscanned until it is in place. Re-run the installer (${installCommand}), or contact your IT administrator with this message." >&2
|
|
300
|
+
echo "ai-scanner: the periodic endpoint-inventory collector could not be installed either: \${UNVERIFIED_REASON:-the download failed}. Re-run the installer (${installCommand}), or contact your IT administrator with this message." >&2
|
|
250
301
|
fi
|
|
251
|
-
exit 0
|
|
252
302
|
fi
|
|
253
303
|
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
|
|
304
|
+
# Neither is in place: nothing below can run, and the message above already said
|
|
305
|
+
# so with its next action.
|
|
306
|
+
[ -x "$BIN" ] || exit 0
|
|
257
307
|
|
|
258
308
|
# Binary exists — throttled self-update check.
|
|
259
309
|
#
|
|
@@ -276,13 +326,38 @@ if should_check_update; then
|
|
|
276
326
|
have_host=$(timeout "$VERSION_TIMEOUT" "$BIN" version 2>/dev/null | sed -n 's#^server: [a-z]*://\\([^/]*\\).*#\\1#p' | head -1)
|
|
277
327
|
if [ -n "$have_host" ] && [ -n "$want_host" ] && [ "$have_host" != "$want_host" ]; then
|
|
278
328
|
echo "ai-scanner: the installed binary talks to $have_host, but this installation provisions from $want_host, so its access key can never be accepted (detection logs are refused and discarded). Re-downloading the matching binary." >&2
|
|
279
|
-
download_binary "\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}" || \\
|
|
329
|
+
download_binary "\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}" "$BIN" || \\
|
|
280
330
|
echo "ai-scanner: could not replace the mismatched binary: \${UNVERIFIED_REASON:-the download failed}. Scanning continues locally, but detection logs will keep being refused. Re-run the installer, or contact your IT administrator with this message." >&2
|
|
281
331
|
elif [ -z "$have_host" ]; then
|
|
282
332
|
echo "ai-scanner: could not read which license server the installed binary uses, so the environment match was NOT checked. If detection logs are being refused, re-run the installer." >&2
|
|
283
333
|
fi
|
|
284
334
|
timeout "$UPDATE_TIMEOUT" "$BIN" update 2>/dev/null || \\
|
|
285
335
|
echo "ai-scanner: the scanner could not update itself just now. It keeps scanning with the rules it already has; if this repeats, re-run the installer or contact your IT administrator." >&2
|
|
336
|
+
|
|
337
|
+
# Keep the hook binary on the SAME build as the periodic one (issue #2366).
|
|
338
|
+
# They come from one crate and share one version, so a skew means an update
|
|
339
|
+
# moved only ai-scanner and left the harness behind.
|
|
340
|
+
#
|
|
341
|
+
# \`"$HARNESS" update\` is deliberately NOT used (issue #2368): that
|
|
342
|
+
# self-update resolves the bare platform key, which serves ai-scanner —
|
|
343
|
+
# running it would overwrite the harness with the periodic binary and take
|
|
344
|
+
# every hook down with it. Refreshing through the same checksum-verified
|
|
345
|
+
# download path avoids that. **Do not "simplify" this into an update call.**
|
|
346
|
+
#
|
|
347
|
+
# \`GAP-UNOBSERVED-OVERWRITE\`: the overwrite itself has NOT been observed.
|
|
348
|
+
# Reproducing it needs \`/verify\` to advertise a newer version, and v0.1.319
|
|
349
|
+
# has zero keys in S3, so the precondition does not exist yet. What is
|
|
350
|
+
# written above is the mechanism read off \`cli/mod.rs::cmd_update\` — which is
|
|
351
|
+
# enough to route around, and NOT enough to call measured.
|
|
352
|
+
if [ -x "$HARNESS" ]; then
|
|
353
|
+
scanner_ver=$(timeout "$VERSION_TIMEOUT" "$BIN" version 2>/dev/null | awk 'NR==1{print $2}')
|
|
354
|
+
harness_ver=$(timeout "$VERSION_TIMEOUT" "$HARNESS" version 2>/dev/null | awk 'NR==1{print $2}')
|
|
355
|
+
if [ -n "$scanner_ver" ] && [ -n "$harness_ver" ] && [ "$scanner_ver" != "$harness_ver" ]; then
|
|
356
|
+
echo "ai-scanner: the tool-call scanner is on $harness_ver while this endpoint is on $scanner_ver. Refreshing it so both run one build." >&2
|
|
357
|
+
download_binary "\${DOWNLOAD_BASE}/ai-harness/\${PLATFORM_SLUG}" "$HARNESS" || \\
|
|
358
|
+
echo "ai-scanner: could not refresh the tool-call scanner: \${UNVERIFIED_REASON:-the download failed}. Scanning continues on $harness_ver; re-run the installer (${installCommand}) if this repeats." >&2
|
|
359
|
+
fi
|
|
360
|
+
fi
|
|
286
361
|
date +%s > "$PULL_STAMP"
|
|
287
362
|
fi
|
|
288
363
|
|
|
@@ -368,7 +443,7 @@ const SCAN_HELPERS = `run_scanner() {
|
|
|
368
443
|
export const SCAN_BASH_SH = `#!/bin/bash
|
|
369
444
|
# PostToolUse hook for Bash: Scan command output.
|
|
370
445
|
# Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
|
|
371
|
-
BIN="\${
|
|
446
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
372
447
|
${UNSCANNED_NOTE}
|
|
373
448
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
374
449
|
|
|
@@ -385,7 +460,7 @@ jq -r '.tool_response // ""' <<<"$INPUT" \\
|
|
|
385
460
|
export const SCAN_BASH_INPUT_SH = `#!/bin/bash
|
|
386
461
|
# PreToolUse hook for Bash: Scan command before execution.
|
|
387
462
|
# Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
|
|
388
|
-
BIN="\${
|
|
463
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
389
464
|
${UNSCANNED_NOTE}
|
|
390
465
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
391
466
|
|
|
@@ -403,7 +478,7 @@ export const SCAN_EDIT_SH = `#!/bin/bash
|
|
|
403
478
|
# PreToolUse hook for Edit/MultiEdit: Scan new content before write.
|
|
404
479
|
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
405
480
|
# Only the Edit-vs-MultiEdit content extraction is Claude Code specific.
|
|
406
|
-
BIN="\${
|
|
481
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
407
482
|
${UNSCANNED_NOTE}
|
|
408
483
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
409
484
|
|
|
@@ -429,7 +504,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
|
429
504
|
export const SCAN_READ_SH = `#!/bin/bash
|
|
430
505
|
# PostToolUse hook for Read: Scan file content Claude just read.
|
|
431
506
|
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
432
|
-
BIN="\${
|
|
507
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
433
508
|
${UNSCANNED_NOTE}
|
|
434
509
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
435
510
|
|
|
@@ -449,7 +524,7 @@ jq -r '.tool_response.file.content // .tool_response // ""' <<<"$INPUT" \\
|
|
|
449
524
|
export const SCAN_EDIT_OUTPUT_SH = `#!/bin/bash
|
|
450
525
|
# PostToolUse hook for Edit/MultiEdit/Write: Scan final file content.
|
|
451
526
|
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
452
|
-
BIN="\${
|
|
527
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
453
528
|
${UNSCANNED_NOTE}
|
|
454
529
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
455
530
|
|
|
@@ -470,7 +545,7 @@ export const SCAN_READ_INPUT_SH = `#!/bin/bash
|
|
|
470
545
|
# This is a thin Claude Code adapter — all scanning logic lives in the Rust
|
|
471
546
|
# binary, including actor resolution, exclude-dir filtering, content cap, and
|
|
472
547
|
# detection-log writing.
|
|
473
|
-
BIN="\${
|
|
548
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
474
549
|
${UNSCANNED_NOTE}
|
|
475
550
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
476
551
|
|
|
@@ -488,7 +563,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
|
488
563
|
export const SCAN_WEBFETCH_SH = `#!/bin/bash
|
|
489
564
|
# PreToolUse hook for WebFetch: Scan URL before Claude fetches it.
|
|
490
565
|
# Thin Claude Code adapter — Rust handles phishing scoring + YARA + detection log.
|
|
491
|
-
BIN="\${
|
|
566
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
492
567
|
${UNSCANNED_NOTE}
|
|
493
568
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
494
569
|
|
|
@@ -505,7 +580,7 @@ run_scanner scan webfetch --url "$URL" </dev/null
|
|
|
505
580
|
export const SCAN_WEBSEARCH_SH = `#!/bin/bash
|
|
506
581
|
# PostToolUse hook for WebSearch: Scan search results for phishing URLs.
|
|
507
582
|
# Thin Claude Code adapter — Rust handles cap, phishing scoring, detection log.
|
|
508
|
-
BIN="\${
|
|
583
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
509
584
|
${UNSCANNED_NOTE}
|
|
510
585
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
511
586
|
|
|
@@ -551,6 +626,12 @@ export function agentEnvSh(accessKey, agentDir) {
|
|
|
551
626
|
return `#!/bin/bash
|
|
552
627
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
553
628
|
export AI_SCANNER_BIN="\${AI_SCANNER_BIN:-$HOME/${agentDir}/bin/ai-scanner}"
|
|
629
|
+
# The hook binary (issue #2366). Every adapter below scans through THIS one:
|
|
630
|
+
# the eight tool-call contexts live in \`ai-harness\`, not in \`ai-scanner\`
|
|
631
|
+
# (which is the periodic endpoint-inventory collector). Two names, never one
|
|
632
|
+
# shared override — pointing a single variable at one product would leave the
|
|
633
|
+
# other silently addressing a file that is not it.
|
|
634
|
+
export AI_HARNESS_BIN="\${AI_HARNESS_BIN:-$HOME/${agentDir}/bin/ai-harness}"
|
|
554
635
|
export AI_SCANNER_ACCESS_KEY="${accessKey}"
|
|
555
636
|
export AI_SCANNER_LEVEL="\${AI_SCANNER_LEVEL:-high}"
|
|
556
637
|
export AI_SCANNER_EXCLUDE_DIRS="\${AI_SCANNER_EXCLUDE_DIRS:-}"
|
|
@@ -569,11 +650,11 @@ ${UNSCANNED_NOTE}
|
|
|
569
650
|
|
|
570
651
|
run_scanner() {
|
|
571
652
|
if command -v timeout >/dev/null 2>&1; then
|
|
572
|
-
timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${
|
|
653
|
+
timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_HARNESS_BIN}" "$@"
|
|
573
654
|
elif command -v gtimeout >/dev/null 2>&1; then
|
|
574
|
-
gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${
|
|
655
|
+
gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_HARNESS_BIN}" "$@"
|
|
575
656
|
else
|
|
576
|
-
"\${
|
|
657
|
+
"\${AI_HARNESS_BIN}" "$@"
|
|
577
658
|
fi
|
|
578
659
|
local rc=$?
|
|
579
660
|
[ $rc -eq 124 ] && return 0
|
|
@@ -593,7 +674,7 @@ source "$SCRIPT_DIR/env.sh"
|
|
|
593
674
|
cat > /dev/null
|
|
594
675
|
bash "$SCRIPT_DIR/start_scanner.sh" 2>/dev/null || true
|
|
595
676
|
cat <<EOF
|
|
596
|
-
{"env":{"AI_SCANNER_BIN":"\${AI_SCANNER_BIN}","AI_SCANNER_ACCESS_KEY":"${accessKey}","AI_SCANNER_LEVEL":"high"}}
|
|
677
|
+
{"env":{"AI_SCANNER_BIN":"\${AI_SCANNER_BIN}","AI_HARNESS_BIN":"\${AI_HARNESS_BIN}","AI_SCANNER_ACCESS_KEY":"${accessKey}","AI_SCANNER_LEVEL":"high"}}
|
|
597
678
|
EOF
|
|
598
679
|
`;
|
|
599
680
|
}
|
|
@@ -601,7 +682,7 @@ EOF
|
|
|
601
682
|
export const CURSOR_PRE_SHELL_SH = `#!/bin/bash
|
|
602
683
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
603
684
|
source "$SCRIPT_DIR/env.sh"
|
|
604
|
-
BIN="$
|
|
685
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
605
686
|
export AI_SCANNER_DETECTION_LOG
|
|
606
687
|
jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
607
688
|
`;
|
|
@@ -609,7 +690,7 @@ jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
|
609
690
|
export const CURSOR_PRE_READ_SH = `#!/bin/bash
|
|
610
691
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
611
692
|
source "$SCRIPT_DIR/env.sh"
|
|
612
|
-
BIN="$
|
|
693
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
613
694
|
export AI_SCANNER_DETECTION_LOG
|
|
614
695
|
FILE_PATH=$(jq -r '.tool_input.file_path // ""')
|
|
615
696
|
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
@@ -619,7 +700,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
|
619
700
|
export const CURSOR_PRE_EDIT_SH = `#!/bin/bash
|
|
620
701
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
621
702
|
source "$SCRIPT_DIR/env.sh"
|
|
622
|
-
BIN="$
|
|
703
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
623
704
|
export AI_SCANNER_DETECTION_LOG
|
|
624
705
|
INPUT=$(cat)
|
|
625
706
|
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
@@ -636,7 +717,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
|
636
717
|
export const CURSOR_POST_SHELL_SH = `#!/bin/bash
|
|
637
718
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
638
719
|
source "$SCRIPT_DIR/env.sh"
|
|
639
|
-
BIN="$
|
|
720
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
640
721
|
export AI_SCANNER_DETECTION_LOG
|
|
641
722
|
INPUT=$(cat)
|
|
642
723
|
COMMAND=$(jq -r '.tool_output // ""' <<<"$INPUT" | head -1 && jq -r '.tool_input.command // ""' <<<"$INPUT")
|
|
@@ -646,7 +727,7 @@ jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan bash-output --command
|
|
|
646
727
|
export const CURSOR_POST_READ_SH = `#!/bin/bash
|
|
647
728
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
648
729
|
source "$SCRIPT_DIR/env.sh"
|
|
649
|
-
BIN="$
|
|
730
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
650
731
|
export AI_SCANNER_DETECTION_LOG
|
|
651
732
|
INPUT=$(cat)
|
|
652
733
|
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
@@ -657,7 +738,7 @@ jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan read-output --file "$F
|
|
|
657
738
|
export const CURSOR_POST_WRITE_SH = `#!/bin/bash
|
|
658
739
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
659
740
|
source "$SCRIPT_DIR/env.sh"
|
|
660
|
-
BIN="$
|
|
741
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
661
742
|
export AI_SCANNER_DETECTION_LOG
|
|
662
743
|
INPUT=$(cat)
|
|
663
744
|
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
@@ -672,7 +753,7 @@ run_scanner scan edit-output --file "$FILE_PATH" < "$FILE_PATH"
|
|
|
672
753
|
export const WINDSURF_ENSURE_SCANNER_SH = `#!/bin/bash
|
|
673
754
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
674
755
|
source "$SCRIPT_DIR/env.sh"
|
|
675
|
-
BIN="$
|
|
756
|
+
BIN="$AI_HARNESS_BIN"
|
|
676
757
|
LOCK_FILE="/tmp/.ai-scanner-ensure.lock"
|
|
677
758
|
if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
|
|
678
759
|
return 0 2>/dev/null || true
|
|
@@ -688,7 +769,7 @@ return 0 2>/dev/null || true
|
|
|
688
769
|
export const WINDSURF_PRE_COMMAND_SH = `#!/bin/bash
|
|
689
770
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
690
771
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
691
|
-
BIN="$
|
|
772
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
692
773
|
export AI_SCANNER_DETECTION_LOG
|
|
693
774
|
CONTENT=$(jq -r '.tool_info.command_line // ""')
|
|
694
775
|
[ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
|
|
@@ -698,7 +779,7 @@ printf '%s' "$CONTENT" | run_scanner scan bash-input
|
|
|
698
779
|
export const WINDSURF_PRE_READ_SH = `#!/bin/bash
|
|
699
780
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
700
781
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
701
|
-
BIN="$
|
|
782
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
702
783
|
export AI_SCANNER_DETECTION_LOG
|
|
703
784
|
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
704
785
|
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
@@ -708,7 +789,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
|
708
789
|
export const WINDSURF_PRE_WRITE_SH = `#!/bin/bash
|
|
709
790
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
710
791
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
711
|
-
BIN="$
|
|
792
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
712
793
|
export AI_SCANNER_DETECTION_LOG
|
|
713
794
|
INPUT=$(cat)
|
|
714
795
|
FILE_PATH=$(jq -r '.tool_info.file_path // ""' <<<"$INPUT")
|
|
@@ -720,7 +801,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
|
720
801
|
export const WINDSURF_POST_READ_SH = `#!/bin/bash
|
|
721
802
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
722
803
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
723
|
-
BIN="$
|
|
804
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
724
805
|
export AI_SCANNER_DETECTION_LOG
|
|
725
806
|
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
726
807
|
[ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
|
|
@@ -730,7 +811,7 @@ run_scanner scan read-output --file "$FILE_PATH" < "$FILE_PATH"
|
|
|
730
811
|
export const WINDSURF_POST_WRITE_SH = `#!/bin/bash
|
|
731
812
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
732
813
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
733
|
-
BIN="$
|
|
814
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
734
815
|
export AI_SCANNER_DETECTION_LOG
|
|
735
816
|
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
736
817
|
[ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
|
|
@@ -773,7 +854,7 @@ exec "$SCRIPT_DIR/start_scanner.sh"
|
|
|
773
854
|
export const KIRO_PRE_SHELL_SH = `#!/bin/bash
|
|
774
855
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
775
856
|
source "$SCRIPT_DIR/env.sh"
|
|
776
|
-
BIN="$
|
|
857
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
777
858
|
export AI_SCANNER_DETECTION_LOG
|
|
778
859
|
jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
779
860
|
`;
|
|
@@ -781,7 +862,7 @@ jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
|
781
862
|
export const KIRO_PRE_READ_SH = `#!/bin/bash
|
|
782
863
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
783
864
|
source "$SCRIPT_DIR/env.sh"
|
|
784
|
-
BIN="$
|
|
865
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
785
866
|
export AI_SCANNER_DETECTION_LOG
|
|
786
867
|
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""')
|
|
787
868
|
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
@@ -791,7 +872,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
|
791
872
|
export const KIRO_PRE_WRITE_SH = `#!/bin/bash
|
|
792
873
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
793
874
|
source "$SCRIPT_DIR/env.sh"
|
|
794
|
-
BIN="$
|
|
875
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
795
876
|
export AI_SCANNER_DETECTION_LOG
|
|
796
877
|
INPUT=$(cat)
|
|
797
878
|
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
|
|
@@ -803,7 +884,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
|
803
884
|
export const KIRO_POST_SHELL_SH = `#!/bin/bash
|
|
804
885
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
805
886
|
source "$SCRIPT_DIR/env.sh"
|
|
806
|
-
BIN="$
|
|
887
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
807
888
|
export AI_SCANNER_DETECTION_LOG
|
|
808
889
|
INPUT=$(cat)
|
|
809
890
|
COMMAND=$(jq -r '.tool_input.command // ""' <<<"$INPUT")
|
|
@@ -814,7 +895,7 @@ printf '%s' "$RESPONSE" | run_scanner scan bash-output --command "$COMMAND"
|
|
|
814
895
|
export const KIRO_POST_READ_SH = `#!/bin/bash
|
|
815
896
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
816
897
|
source "$SCRIPT_DIR/env.sh"
|
|
817
|
-
BIN="$
|
|
898
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
818
899
|
export AI_SCANNER_DETECTION_LOG
|
|
819
900
|
INPUT=$(cat)
|
|
820
901
|
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""' <<<"$INPUT")
|
|
@@ -826,7 +907,7 @@ printf '%s' "$RESPONSE" | run_scanner scan read-output --file "$FILE_PATH"
|
|
|
826
907
|
export const KIRO_POST_WRITE_SH = `#!/bin/bash
|
|
827
908
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
828
909
|
source "$SCRIPT_DIR/env.sh"
|
|
829
|
-
BIN="$
|
|
910
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
830
911
|
export AI_SCANNER_DETECTION_LOG
|
|
831
912
|
INPUT=$(cat)
|
|
832
913
|
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
|
|
@@ -902,7 +983,7 @@ export const CODEX_SESSION_INIT_SH = `#!/bin/bash
|
|
|
902
983
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
903
984
|
source "$SCRIPT_DIR/env.sh"
|
|
904
985
|
cat > /dev/null # drain stdin so Codex doesn't block on a write
|
|
905
|
-
BIN="$
|
|
986
|
+
BIN="$AI_HARNESS_BIN"
|
|
906
987
|
LOCK_FILE="/tmp/.ai-scanner-codex-init.lock"
|
|
907
988
|
if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
|
|
908
989
|
exit 0
|
|
@@ -919,7 +1000,7 @@ export const CODEX_PRE_SHELL_SH = `#!/bin/bash
|
|
|
919
1000
|
# Codex PreToolUse Bash hook: scan command and return block JSON to Codex.
|
|
920
1001
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
921
1002
|
source "$SCRIPT_DIR/env.sh"
|
|
922
|
-
BIN="$
|
|
1003
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
923
1004
|
export AI_SCANNER_DETECTION_LOG
|
|
924
1005
|
INPUT=$(cat)
|
|
925
1006
|
CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
|
|
@@ -949,7 +1030,7 @@ export const CODEX_POST_SHELL_SH = `#!/bin/bash
|
|
|
949
1030
|
# Codex PostToolUse Bash hook: scan command output and return feedback JSON.
|
|
950
1031
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
951
1032
|
source "$SCRIPT_DIR/env.sh"
|
|
952
|
-
BIN="$
|
|
1033
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
953
1034
|
export AI_SCANNER_DETECTION_LOG
|
|
954
1035
|
INPUT=$(cat)
|
|
955
1036
|
CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
|