@coworker-jp/aidr 0.1.318 → 0.1.323
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 +10 -5
- 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 +262 -52
- package/src/toml-merge.mjs +117 -29
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 = {
|
|
@@ -49,8 +52,10 @@ const CODEX_AGENTS_MD = `# AIDR Scanner
|
|
|
49
52
|
|
|
50
53
|
AIDR security scanner is active for this Codex session. Set the access key
|
|
51
54
|
via the \`AI_SCANNER_ACCESS_KEY\` environment variable. Hooks are registered
|
|
52
|
-
in \`.codex/hooks.json\` and require \`[features]
|
|
53
|
-
\`.codex/config.toml\` (this setup writes both for you).
|
|
55
|
+
in \`.codex/hooks.json\` and require \`[features] hooks = true\` in
|
|
56
|
+
\`.codex/config.toml\` (this setup writes both for you). The older
|
|
57
|
+
\`[features] codex_hooks\` spelling is deprecated by Codex — if your config
|
|
58
|
+
still has it, setup renames it to \`hooks\` in place.
|
|
54
59
|
|
|
55
60
|
Codex currently emits Bash tool events only. Read / Edit / WebFetch / WebSearch
|
|
56
61
|
are not hookable — for those, use Claude Code.
|
|
@@ -180,7 +185,7 @@ async function mergeCodexConfigToml(target, { dryRun, stderr = process.stderr })
|
|
|
180
185
|
if (stat) {
|
|
181
186
|
stderr.write(
|
|
182
187
|
`WARNING: ${target} already exists. (dry-run: would back up)\n` +
|
|
183
|
-
`Merging [features] codex_hooks; your other settings are preserved.\n`
|
|
188
|
+
`Merging [features] hooks (the deprecated codex_hooks key is migrated if present); your other settings are preserved.\n`
|
|
184
189
|
);
|
|
185
190
|
}
|
|
186
191
|
process.stdout.write(
|
|
@@ -196,7 +201,7 @@ async function mergeCodexConfigToml(target, { dryRun, stderr = process.stderr })
|
|
|
196
201
|
await fsp.copyFile(target, backupPath);
|
|
197
202
|
stderr.write(
|
|
198
203
|
`WARNING: ${target} already exists. Backed up to ${backupPath}.\n` +
|
|
199
|
-
`Merging [features] codex_hooks; your other settings are preserved.\n`
|
|
204
|
+
`Merging [features] hooks (the deprecated codex_hooks key is migrated if present); your other settings are preserved.\n`
|
|
200
205
|
);
|
|
201
206
|
}
|
|
202
207
|
|
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
|
@@ -11,11 +11,44 @@ export function getSettingsJson(accessKey, scope = "project") {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// start_scanner.sh — parameterized by the verify endpoint (prod / dev).
|
|
14
|
+
// ── このスクリプトが端末へ出す文言は ja / en を併記する (issue #2372 条件 3) ──
|
|
15
|
+
//
|
|
16
|
+
// **決定 (2026-09-09): (a) 併記する。** `docs/product/UI_COPY.md` の表の
|
|
17
|
+
// 「`aidr` CLI (端末のターミナルに出る文字列)」の行が既にこう決めており、
|
|
18
|
+
// **その行の理由がこの面にそのまま当たる**:
|
|
19
|
+
//
|
|
20
|
+
// * **読み手を実行時に特定できない。** 同じ端末で情シスも開発者も読む。
|
|
21
|
+
// * **出し分ける先が無い。** フックはロケールを持たない bash なので、
|
|
22
|
+
// 1 つの出力を全員が読む。**片方だけにすると、読めない側にとっては
|
|
23
|
+
// 何も書かれていないのと同じである。**
|
|
24
|
+
//
|
|
25
|
+
// この 2 つは「利用者が打ったコマンド」かどうかに依存しない。むしろこの面は
|
|
26
|
+
// CLI より弱い —— **利用者はこの出力を頼んでいない**ので、読めない言語で
|
|
27
|
+
// 出された行は、そのまま読み飛ばされる。
|
|
28
|
+
//
|
|
29
|
+
// **(b)「フックの stderr は en のみ」と明文化する案は採らなかった。** 採れば
|
|
30
|
+
// この面だけが表の 4 行のどれとも違う基準を持つことになり、**なぜここだけ
|
|
31
|
+
// 違うのかを説明できない** (英語だけだったのは判断の結果ではなく、
|
|
32
|
+
// 基準を持たないまま書かれ続けた結果である —— `UI_COPY.md` の
|
|
33
|
+
// `GAP-2355-CLI-COPY-JA-EN-UNEVEN` がその状態を名指ししている)。
|
|
34
|
+
//
|
|
35
|
+
// **★ 短くするために事実や次の行動を削らないこと** (`UI_COPY.md` 【非交渉】)。
|
|
36
|
+
// 1 文が長くなるなら**文を割る**。en 行はそのまま残してある —— 併記は
|
|
37
|
+
// 「英語を日本語で置き換える」ことではない。
|
|
38
|
+
//
|
|
39
|
+
// **ここで揃うのはこのスクリプトだけである** (`GAP-2372-AIDR-JA-EN-PARTIAL`)。
|
|
40
|
+
// `packages/aidr` の他の出力 (`red-team.mjs::redTeamInstallFailureText` など) は
|
|
41
|
+
// 英語のままで、揃えるのは別の作業である。**「aidr は併記済み」と読まないこと。**
|
|
14
42
|
// `agentType` is currently unused inside the script (endpoint-info has moved
|
|
15
43
|
// to schedule-only invocation via aidr --scheduled), but the parameter
|
|
16
44
|
// is kept for signature stability with callers and the wider TS template.
|
|
17
45
|
// eslint-disable-next-line no-unused-vars
|
|
18
46
|
export function getStartScannerSh(downloadBase = "https://pytd2jd5p7.execute-api.ap-northeast-1.amazonaws.com/download", agentType = "claude-code") {
|
|
47
|
+
// `agentType` is the endpoint-info spelling (`claude-code`); `setup --agent`
|
|
48
|
+
// takes the installer/portal spelling (`claude`). They differ for exactly one
|
|
49
|
+
// agent, so the mapping is stated here rather than guessed at the call site —
|
|
50
|
+
// guessing would make the self-repair below configure the WRONG agent.
|
|
51
|
+
const setupAgentName = agentType === "claude-code" ? "claude" : agentType;
|
|
19
52
|
// Which installer to quote back at the user, derived from the endpoint this
|
|
20
53
|
// script was generated against. The DEV package must never tell anyone to
|
|
21
54
|
// install PROD (same rule as cli.mjs's INSTALL_COMMAND_HINT) — and here the
|
|
@@ -32,6 +65,16 @@ export function getStartScannerSh(downloadBase = "https://pytd2jd5p7.execute-api
|
|
|
32
65
|
# - Fail-open: never block Claude Code startup
|
|
33
66
|
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
34
67
|
BIN_DIR="$(dirname "$BIN")"
|
|
68
|
+
# TWO binaries since the split (issue #2366). \`ai-harness\` runs the eight
|
|
69
|
+
# tool-call scan contexts — it is the one every hook adapter invokes, so it is
|
|
70
|
+
# the one whose absence means "this session is not being scanned". \`ai-scanner\`
|
|
71
|
+
# is the periodic inventory collector and the self-update vehicle. They are
|
|
72
|
+
# built from one crate and therefore must stay on ONE version; the skew check
|
|
73
|
+
# further down is what enforces that.
|
|
74
|
+
#
|
|
75
|
+
# Two env names, never one shared: pointing a single override at one product
|
|
76
|
+
# would silently leave the other pointing at a file that is not it.
|
|
77
|
+
HARNESS="\${AI_HARNESS_BIN:-$BIN_DIR/ai-harness}"
|
|
35
78
|
# The self-update throttle belongs to the binary it throttles, so it is derived
|
|
36
79
|
# from \$BIN — the one source this script already trusts for "where is my copy"
|
|
37
80
|
# (\`.ai-scanner-unscanned\` is placed the same way). It used to be pinned to
|
|
@@ -132,7 +175,8 @@ sha256_of() {
|
|
|
132
175
|
# \`$UNVERIFIED_REASON\` carries WHY, so the message can name it.
|
|
133
176
|
download_binary() {
|
|
134
177
|
local url="$1"
|
|
135
|
-
local
|
|
178
|
+
local dest="$2"
|
|
179
|
+
local tmp=$(mktemp "\${BIN_DIR}/.download.XXXXXX")
|
|
136
180
|
local auth_args=()
|
|
137
181
|
if [ -n "\${AI_SCANNER_ACCESS_KEY:-}" ]; then
|
|
138
182
|
auth_args=(-H "x-access-key: \${AI_SCANNER_ACCESS_KEY}")
|
|
@@ -166,10 +210,34 @@ download_binary() {
|
|
|
166
210
|
# macOS: strip Gatekeeper quarantine so unsigned binary can execute
|
|
167
211
|
# (no-op on Linux where xattr isn't typically present)
|
|
168
212
|
xattr -d com.apple.quarantine "$tmp" 2>/dev/null || true
|
|
169
|
-
mv -f "$tmp" "$
|
|
213
|
+
mv -f "$tmp" "$dest"
|
|
170
214
|
return 0
|
|
171
215
|
}
|
|
172
216
|
|
|
217
|
+
# Fetch one product into place if it is not already there. The download route
|
|
218
|
+
# serves \`ai-scanner\` at the bare platform key (unchanged wire contract) and
|
|
219
|
+
# every other product under its own name.
|
|
220
|
+
#
|
|
221
|
+
# Says "installed" only when it actually installed something. Printing it on
|
|
222
|
+
# every session start for a file that was already there turns the line into
|
|
223
|
+
# noise, and the next reader stops distinguishing it from the one that matters.
|
|
224
|
+
# $1 = absolute destination, $2 = product name
|
|
225
|
+
ensure_binary() {
|
|
226
|
+
local dest="$1" product="$2" url
|
|
227
|
+
[ -x "$dest" ] && return 0
|
|
228
|
+
if [ "$product" = "ai-scanner" ]; then
|
|
229
|
+
url="\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}"
|
|
230
|
+
else
|
|
231
|
+
url="\${DOWNLOAD_BASE}/\${product}/\${PLATFORM_SLUG}"
|
|
232
|
+
fi
|
|
233
|
+
if download_binary "$url" "$dest"; then
|
|
234
|
+
echo "$product のバイナリを導入しました (\${PLATFORM_SLUG}、チェックサム検証済み)。" >&2
|
|
235
|
+
echo "$product binary installed (\${PLATFORM_SLUG}, checksum verified)" >&2
|
|
236
|
+
return 0
|
|
237
|
+
fi
|
|
238
|
+
return 1
|
|
239
|
+
}
|
|
240
|
+
|
|
173
241
|
# What has already run unscanned on this machine, if anything (#1774). The scan
|
|
174
242
|
# hooks append to this file every time they exit without a binary; this is the
|
|
175
243
|
# first moment anyone can say it out loud, and it is deleted the moment a binary
|
|
@@ -187,6 +255,7 @@ report_unscanned_history() {
|
|
|
187
255
|
# itself is the fallback — printing nothing here would drop the fact.
|
|
188
256
|
local when
|
|
189
257
|
when=$(date -d "@$first" 2>/dev/null || date -r "$first" 2>/dev/null || echo "epoch $first")
|
|
258
|
+
echo "ai-scanner: この端末では、すでに $count 件のツールコールが検査されないまま実行されています (最初は $when)。" >&2
|
|
190
259
|
echo "ai-scanner: $count tool call(s) have already run unscanned on this machine (first at $when)." >&2
|
|
191
260
|
}
|
|
192
261
|
|
|
@@ -195,8 +264,10 @@ report_unscanned_history() {
|
|
|
195
264
|
# reads as cosmetic (docs/product/UI_COPY.md 【非交渉】) — and unlike a failed
|
|
196
265
|
# download, re-running the installer cannot fix it, so the action is different.
|
|
197
266
|
unsupported_platform() {
|
|
267
|
+
echo "ai-scanner: このセッションは検査されていません。このプラットフォーム ($1) 向けのスキャナーが存在しません。" >&2
|
|
198
268
|
echo "ai-scanner: this session is NOT being scanned — there is no scanner build for this platform ($1)." >&2
|
|
199
269
|
report_unscanned_history
|
|
270
|
+
echo "ai-scanner: ツールコールは動き続けますが、この端末では検査されないまま実行されます。このメッセージを添えて、社内の IT 管理者にご連絡ください。" >&2
|
|
200
271
|
echo "ai-scanner: tool calls keep working, and they run unscanned on this machine. Contact your IT administrator with this message." >&2
|
|
201
272
|
}
|
|
202
273
|
|
|
@@ -234,26 +305,46 @@ case "$OS" in
|
|
|
234
305
|
;;
|
|
235
306
|
esac
|
|
236
307
|
|
|
237
|
-
#
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
308
|
+
# The HOOK binary first: tool-call scanning depends on this one, so its absence
|
|
309
|
+
# is what the session-level message is about.
|
|
310
|
+
#
|
|
311
|
+
# docs/product/UI_COPY.md 【非交渉】: the fact, stated so it cannot be read as
|
|
312
|
+
# harmless, and then the next action. The old single line ("Warning: Failed to
|
|
313
|
+
# download …") said neither what it meant for the session nor what to do, and it
|
|
314
|
+
# was the ONLY trace anywhere (#1774).
|
|
315
|
+
SCANNING=0
|
|
316
|
+
if ensure_binary "$HARNESS" ai-harness; then
|
|
317
|
+
SCANNING=1
|
|
318
|
+
# A binary IS in place. Clear any record of having run unscanned, so the
|
|
319
|
+
# record only ever describes the machine's CURRENT state.
|
|
320
|
+
rm -f "$UNSCANNED_STAMP"
|
|
321
|
+
else
|
|
322
|
+
echo "ai-scanner: このセッションは検査されていません。ツールコール検査機 (ai-harness) がこの端末になく、今回も導入できませんでした: \${UNVERIFIED_REASON:-ダウンロードに失敗しました}。" >&2
|
|
323
|
+
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
|
|
324
|
+
report_unscanned_history
|
|
325
|
+
echo "ai-scanner: ツールコールは動き続けますが、導入されるまで検査されないまま実行されます。導入コマンド (${installCommand}) をもう一度実行してください。それでも直らなければ、このメッセージを添えて社内の IT 管理者にご連絡ください。" >&2
|
|
326
|
+
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
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
# The PERIODIC binary. Its absence does NOT stop tool-call scanning, so it gets
|
|
330
|
+
# its own, narrower sentence rather than borrowing the one above — saying "this
|
|
331
|
+
# session is NOT being scanned" when it IS would train the reader to ignore the
|
|
332
|
+
# line that means it.
|
|
333
|
+
if ! ensure_binary "$BIN" ai-scanner; then
|
|
334
|
+
# Only claims scanning is unaffected when it actually is — the harness may
|
|
335
|
+
# have failed too, and the line above is the one that says so.
|
|
336
|
+
if [ "$SCANNING" = 1 ]; then
|
|
337
|
+
echo "ai-scanner: ツールコールの検査は続いています。ただし定期の端末情報収集は導入できませんでした: \${UNVERIFIED_REASON:-ダウンロードに失敗しました}。この端末がポータルに現れなくなったら、導入コマンド (${installCommand}) をもう一度実行してください。" >&2
|
|
338
|
+
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
339
|
else
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
340
|
+
echo "ai-scanner: 定期の端末情報収集も導入できませんでした: \${UNVERIFIED_REASON:-ダウンロードに失敗しました}。導入コマンド (${installCommand}) をもう一度実行してください。それでも直らなければ、このメッセージを添えて社内の IT 管理者にご連絡ください。" >&2
|
|
341
|
+
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
342
|
fi
|
|
251
|
-
exit 0
|
|
252
343
|
fi
|
|
253
344
|
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
|
|
345
|
+
# Neither is in place: nothing below can run, and the message above already said
|
|
346
|
+
# so with its next action.
|
|
347
|
+
[ -x "$BIN" ] || exit 0
|
|
257
348
|
|
|
258
349
|
# Binary exists — throttled self-update check.
|
|
259
350
|
#
|
|
@@ -275,17 +366,130 @@ if should_check_update; then
|
|
|
275
366
|
want_host=$(printf '%s' "$DOWNLOAD_BASE" | sed -E 's#^[a-z]+://([^/]+).*#\\1#')
|
|
276
367
|
have_host=$(timeout "$VERSION_TIMEOUT" "$BIN" version 2>/dev/null | sed -n 's#^server: [a-z]*://\\([^/]*\\).*#\\1#p' | head -1)
|
|
277
368
|
if [ -n "$have_host" ] && [ -n "$want_host" ] && [ "$have_host" != "$want_host" ]; then
|
|
369
|
+
echo "ai-scanner: 導入済みのバイナリは $have_host を見ていますが、この導入は $want_host から配信しています。アクセスキーは決して受け付けられず、検知ログは拒否されて捨てられます。一致するバイナリを取り直しています。" >&2
|
|
278
370
|
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}" || \\
|
|
371
|
+
download_binary "\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}" "$BIN" || \\
|
|
372
|
+
echo "ai-scanner: 食い違ったバイナリを置き換えられませんでした: \${UNVERIFIED_REASON:-ダウンロードに失敗しました}。手元の検査は続きますが、検知ログは拒否され続けます。導入コマンドをもう一度実行してください。それでも直らなければ、このメッセージを添えて社内の IT 管理者にご連絡ください。" >&2
|
|
280
373
|
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
374
|
elif [ -z "$have_host" ]; then
|
|
375
|
+
echo "ai-scanner: 導入済みのバイナリがどのライセンスサーバーを見ているか読めなかったので、環境が一致するかは確認していません。検知ログが拒否されているようなら、導入コマンドをもう一度実行してください。" >&2
|
|
282
376
|
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
377
|
fi
|
|
284
378
|
timeout "$UPDATE_TIMEOUT" "$BIN" update 2>/dev/null || \\
|
|
379
|
+
echo "ai-scanner: スキャナーは今回、自分を更新できませんでした。すでに持っているルールで検査は続きます。これが繰り返されるなら、導入コマンドをもう一度実行してください。それでも直らなければ社内の IT 管理者にご連絡ください。" >&2
|
|
285
380
|
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
|
|
381
|
+
|
|
382
|
+
# Keep the hook binary on the SAME build as the periodic one (issue #2366).
|
|
383
|
+
# They come from one crate and share one version, so a skew means an update
|
|
384
|
+
# moved only ai-scanner and left the harness behind.
|
|
385
|
+
#
|
|
386
|
+
# \`"$HARNESS" update\` is deliberately NOT used (issue #2368): that
|
|
387
|
+
# self-update resolves the bare platform key, which serves ai-scanner —
|
|
388
|
+
# running it would overwrite the harness with the periodic binary and take
|
|
389
|
+
# every hook down with it. Refreshing through the same checksum-verified
|
|
390
|
+
# download path avoids that. **Do not "simplify" this into an update call.**
|
|
391
|
+
#
|
|
392
|
+
# \`GAP-UNOBSERVED-OVERWRITE\`: the overwrite itself has NOT been observed.
|
|
393
|
+
# Reproducing it needs \`/verify\` to advertise a newer version, and v0.1.319
|
|
394
|
+
# has zero keys in S3, so the precondition does not exist yet. What is
|
|
395
|
+
# written above is the mechanism read off \`cli/mod.rs::cmd_update\` — which is
|
|
396
|
+
# enough to route around, and NOT enough to call measured.
|
|
397
|
+
if [ -x "$HARNESS" ]; then
|
|
398
|
+
scanner_ver=$(timeout "$VERSION_TIMEOUT" "$BIN" version 2>/dev/null | awk 'NR==1{print $2}')
|
|
399
|
+
harness_ver=$(timeout "$VERSION_TIMEOUT" "$HARNESS" version 2>/dev/null | awk 'NR==1{print $2}')
|
|
400
|
+
if [ -n "$scanner_ver" ] && [ -n "$harness_ver" ] && [ "$scanner_ver" != "$harness_ver" ]; then
|
|
401
|
+
echo "ai-scanner: ツールコール検査機は $harness_ver、この端末は $scanner_ver です。同じビルドで揃うよう、いま取り直しています。" >&2
|
|
402
|
+
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
|
|
403
|
+
download_binary "\${DOWNLOAD_BASE}/ai-harness/\${PLATFORM_SLUG}" "$HARNESS" || \\
|
|
404
|
+
echo "ai-scanner: ツールコール検査機を取り直せませんでした: \${UNVERIFIED_REASON:-ダウンロードに失敗しました}。検査は $harness_ver のまま続きます。これが繰り返されるなら、導入コマンド (${installCommand}) をもう一度実行してください。" >&2
|
|
405
|
+
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
|
|
406
|
+
fi
|
|
407
|
+
fi
|
|
286
408
|
date +%s > "$PULL_STAMP"
|
|
287
409
|
fi
|
|
288
410
|
|
|
411
|
+
# ── 既に撒かれたラッパーを現行の形へ収束させる (issue #2372、案 1) ────────────
|
|
412
|
+
#
|
|
413
|
+
# **バイナリは自動で届くが、設定は届かない。** アダプタは導入時に 1 度だけ
|
|
414
|
+
# 書かれ、以後どの経路でも書き換わらない一方、自己更新はそれが名指すバイナリを
|
|
415
|
+
# 黙って差し替える。その非対称が #2372 の本体で、ここはその片側を閉じる。
|
|
416
|
+
#
|
|
417
|
+
# **なぜ SessionStart なのか (同意の範囲)**: これは**利用者が自分でエディタを
|
|
418
|
+
# 起動したときにだけ、その利用者として**走る。「そのエディタのフックを直す」と
|
|
419
|
+
# いう範囲から出ない。常駐が利用者の home を書く案 (#2372 案 2) は
|
|
420
|
+
# **同意の範囲の判断なので採らない** —— --agent が常駐を入れないという既存の
|
|
421
|
+
# 判断の、ちょうど裏返しである。
|
|
422
|
+
#
|
|
423
|
+
# **★ この経路が届かない端末が在る (正直に置く)。** 収束させるコードはこの
|
|
424
|
+
# スクリプトの中に在るので、**この版より前の start_scanner.sh を持つ端末には
|
|
425
|
+
# 届かない。** そこは agent_hooks の hook_wiring (endpoint-info) が
|
|
426
|
+
# **数として見えるようにする**部分が受け持つ —— だから #2372 の案 3 は
|
|
427
|
+
# 「あとで」ではなく必須である。GAP-2372-PRE-REPAIR-ADAPTERS-UNREACHED。
|
|
428
|
+
#
|
|
429
|
+
# 4 つの門。**どれも「直せない」を「直した」と読ませないため**にある:
|
|
430
|
+
# 1. hook のバイナリが在る (無ければ上の節が既に名指している)
|
|
431
|
+
# 2. その版が setup を持つ (旧い版は持たない。持たないことは失敗ではない)
|
|
432
|
+
# 3. アクセスキーが env に在る —— **無いまま書くと、鍵の無い settings.json を
|
|
433
|
+
# 置くことになり、いま効いている保護を壊す**
|
|
434
|
+
# 4. 兄弟のラッパーが実際に旧い形である (新しい形なら 1 バイトも書かない)
|
|
435
|
+
CONFIG_ROOT="$(cd "$(dirname "$0")/../.." 2>/dev/null && pwd)"
|
|
436
|
+
SELF_DIR="$(dirname "$0")"
|
|
437
|
+
if [ -x "$HARNESS" ] && [ -n "$CONFIG_ROOT" ] && [ -n "\${AI_SCANNER_ACCESS_KEY:-}" ]; then
|
|
438
|
+
# 旧い形の判定は endpoint-info の hook_wiring と**同じ集合・同じ述語**である
|
|
439
|
+
# (agent_hooks.rs の HookWiring::from_wrapper_dir / from_wrapper_text)。
|
|
440
|
+
# 2 つ目の判定を作らない ——作ると、ポータルが数えている集合と、
|
|
441
|
+
# ここが直す集合がずれる。
|
|
442
|
+
#
|
|
443
|
+
# **【2026-09-10 訂正 —— issue #2407】かつてこの行は「同じ述語である」と
|
|
444
|
+
# 宣言しながら、実体は別のファイルを読んでいた。** 数える側は印
|
|
445
|
+
# (\`hook_marker\`) 1 本を読んでおり、claude-code のそれは
|
|
446
|
+
# **\`start_scanner.sh\` そのもの** = 分割後のビルドでは必ず \`ai-harness\` を
|
|
447
|
+
# 名指すスクリプトである。一方ここが直すのは**その兄弟**なので、
|
|
448
|
+
# **「新しい start_scanner.sh + 旧い兄弟」= まさに収束に失敗した端末が
|
|
449
|
+
# \`ai-harness\` (移行済み) として数から消えていた** (DDB 読み戻しで実測)。
|
|
450
|
+
# **数える側をこの集合へ合わせて直した** —— 直す側に \`start_scanner.sh\` を
|
|
451
|
+
# 足すのは、数える側の食い違いを 1 ミリも動かさない。
|
|
452
|
+
#
|
|
453
|
+
# **下の 4 行 (glob / 除外の case / 2 本の grep) は実体から抜き出されて
|
|
454
|
+
# 機械照合される** —— \`agent_hooks::tests::w1_the_repair_loop_and_this_module_read_one_set\`
|
|
455
|
+
# が綴りだけでなく**向き** (\`|| continue\` か \`|| stale=1\` か) まで見る。
|
|
456
|
+
# 書き換えるときは、あちらの \`WIRING_EXCLUDED_WRAPPERS\` /
|
|
457
|
+
# \`SCANNER_TOKENS\` / \`HARNESS_TOKENS\` も同じ PR で直すこと。
|
|
458
|
+
# なぜ grep -E なのか (issue #2372。実測で 1 度落ちている)。
|
|
459
|
+
# ここは長く BRE の grep -q 'ai-scanner\\|AI_SCANNER_BIN' と書かれていたが、
|
|
460
|
+
# **この本文は JS のテンプレートリテラルなので、書いたバックスラッシュ 1 つは
|
|
461
|
+
# 出力に残らない** —— 端末に置かれていたのは 'ai-scanner|AI_SCANNER_BIN' で、
|
|
462
|
+
# BRE の | はただの文字なので **この行は決して一致せず、収束は 1 度も
|
|
463
|
+
# 走らなかった** (2026-09-09 に linux-testbed で実測: 旧い形のラッパーを
|
|
464
|
+
# 置いて start_scanner.sh を回しても 1 バイトも書き換わらない)。
|
|
465
|
+
# **症状は出ない** —— 収束しないことと「収束すべきものが無い」ことが
|
|
466
|
+
# 端末側から区別できない。-E にすれば JS の逃がしが要らず (パターンに
|
|
467
|
+
# バックスラッシュが 1 つも無い)、BSD grep (macOS) が BRE の \\| を
|
|
468
|
+
# 解さない問題も同時に消える。**BRE へ戻さないこと。**
|
|
469
|
+
stale=0
|
|
470
|
+
for w in "$SELF_DIR"/*.sh; do
|
|
471
|
+
[ -r "$w" ] || continue
|
|
472
|
+
case "$w" in *"/start_scanner.sh"|*"/stop_scanner.sh") continue ;; esac
|
|
473
|
+
grep -Eq 'ai-scanner|AI_SCANNER_BIN' "$w" || continue
|
|
474
|
+
grep -Eq 'ai-harness|AI_HARNESS_BIN' "$w" || stale=1
|
|
475
|
+
done
|
|
476
|
+
if [ "$stale" = 1 ] && timeout "$VERSION_TIMEOUT" "$HARNESS" setup --help >/dev/null 2>&1; then
|
|
477
|
+
# --home を渡すので根の解決は起きない。--scope は settings.json に焼く
|
|
478
|
+
# 根の綴り ("$CLAUDE_PROJECT_DIR" か "$HOME") を決めるだけなので、
|
|
479
|
+
# **いまの設置場所から導く** —— 推測すると、リポジトリに置いた設定が
|
|
480
|
+
# 端末個別の設定として書き直される。
|
|
481
|
+
if [ "$CONFIG_ROOT" = "$HOME" ]; then repair_scope=user; else repair_scope=project; fi
|
|
482
|
+
echo "ai-scanner: この端末のフックアダプタは分割前の形で、ツールコール検査機ではなく定期実行のバイナリを呼んでいます。いま更新しています。お手元での操作は要りません。" >&2
|
|
483
|
+
echo "ai-scanner: this endpoint's hook adapters are the pre-split shape, so they still call the periodic binary instead of the tool-call scanner. Updating them now; no action is needed from you." >&2
|
|
484
|
+
timeout "$UPDATE_TIMEOUT" "$HARNESS" setup \\
|
|
485
|
+
--agent ${setupAgentName} --scope "$repair_scope" \\
|
|
486
|
+
--access-key "$AI_SCANNER_ACCESS_KEY" \\
|
|
487
|
+
--download-base "$DOWNLOAD_BASE" --home "$CONFIG_ROOT" >/dev/null 2>&1 || \\
|
|
488
|
+
echo "ai-scanner: この端末のフックアダプタは分割前の形のままで、今回は更新できませんでした。ツールコールは、すでに導入済みのバイナリが検査し続けます。このメッセージが繰り返されるなら、導入コマンド (${installCommand}) をもう一度実行してください。それでも直らなければ、このメッセージを添えて社内の IT 管理者にご連絡ください。" >&2
|
|
489
|
+
echo "ai-scanner: the hook adapters on this endpoint are still the pre-split shape and could not be updated just now. Tool calls keep being scanned by the binary already installed; if this message repeats, re-run the installer (${installCommand}) or contact your IT administrator with this message." >&2
|
|
490
|
+
fi
|
|
491
|
+
fi
|
|
492
|
+
|
|
289
493
|
exit 0
|
|
290
494
|
`;
|
|
291
495
|
}
|
|
@@ -368,7 +572,7 @@ const SCAN_HELPERS = `run_scanner() {
|
|
|
368
572
|
export const SCAN_BASH_SH = `#!/bin/bash
|
|
369
573
|
# PostToolUse hook for Bash: Scan command output.
|
|
370
574
|
# Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
|
|
371
|
-
BIN="\${
|
|
575
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
372
576
|
${UNSCANNED_NOTE}
|
|
373
577
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
374
578
|
|
|
@@ -385,7 +589,7 @@ jq -r '.tool_response // ""' <<<"$INPUT" \\
|
|
|
385
589
|
export const SCAN_BASH_INPUT_SH = `#!/bin/bash
|
|
386
590
|
# PreToolUse hook for Bash: Scan command before execution.
|
|
387
591
|
# Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
|
|
388
|
-
BIN="\${
|
|
592
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
389
593
|
${UNSCANNED_NOTE}
|
|
390
594
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
391
595
|
|
|
@@ -403,7 +607,7 @@ export const SCAN_EDIT_SH = `#!/bin/bash
|
|
|
403
607
|
# PreToolUse hook for Edit/MultiEdit: Scan new content before write.
|
|
404
608
|
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
405
609
|
# Only the Edit-vs-MultiEdit content extraction is Claude Code specific.
|
|
406
|
-
BIN="\${
|
|
610
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
407
611
|
${UNSCANNED_NOTE}
|
|
408
612
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
409
613
|
|
|
@@ -429,7 +633,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
|
429
633
|
export const SCAN_READ_SH = `#!/bin/bash
|
|
430
634
|
# PostToolUse hook for Read: Scan file content Claude just read.
|
|
431
635
|
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
432
|
-
BIN="\${
|
|
636
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
433
637
|
${UNSCANNED_NOTE}
|
|
434
638
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
435
639
|
|
|
@@ -449,7 +653,7 @@ jq -r '.tool_response.file.content // .tool_response // ""' <<<"$INPUT" \\
|
|
|
449
653
|
export const SCAN_EDIT_OUTPUT_SH = `#!/bin/bash
|
|
450
654
|
# PostToolUse hook for Edit/MultiEdit/Write: Scan final file content.
|
|
451
655
|
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
452
|
-
BIN="\${
|
|
656
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
453
657
|
${UNSCANNED_NOTE}
|
|
454
658
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
455
659
|
|
|
@@ -470,7 +674,7 @@ export const SCAN_READ_INPUT_SH = `#!/bin/bash
|
|
|
470
674
|
# This is a thin Claude Code adapter — all scanning logic lives in the Rust
|
|
471
675
|
# binary, including actor resolution, exclude-dir filtering, content cap, and
|
|
472
676
|
# detection-log writing.
|
|
473
|
-
BIN="\${
|
|
677
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
474
678
|
${UNSCANNED_NOTE}
|
|
475
679
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
476
680
|
|
|
@@ -488,7 +692,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
|
488
692
|
export const SCAN_WEBFETCH_SH = `#!/bin/bash
|
|
489
693
|
# PreToolUse hook for WebFetch: Scan URL before Claude fetches it.
|
|
490
694
|
# Thin Claude Code adapter — Rust handles phishing scoring + YARA + detection log.
|
|
491
|
-
BIN="\${
|
|
695
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
492
696
|
${UNSCANNED_NOTE}
|
|
493
697
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
494
698
|
|
|
@@ -505,7 +709,7 @@ run_scanner scan webfetch --url "$URL" </dev/null
|
|
|
505
709
|
export const SCAN_WEBSEARCH_SH = `#!/bin/bash
|
|
506
710
|
# PostToolUse hook for WebSearch: Scan search results for phishing URLs.
|
|
507
711
|
# Thin Claude Code adapter — Rust handles cap, phishing scoring, detection log.
|
|
508
|
-
BIN="\${
|
|
712
|
+
BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
|
|
509
713
|
${UNSCANNED_NOTE}
|
|
510
714
|
[ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
511
715
|
|
|
@@ -551,6 +755,12 @@ export function agentEnvSh(accessKey, agentDir) {
|
|
|
551
755
|
return `#!/bin/bash
|
|
552
756
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
553
757
|
export AI_SCANNER_BIN="\${AI_SCANNER_BIN:-$HOME/${agentDir}/bin/ai-scanner}"
|
|
758
|
+
# The hook binary (issue #2366). Every adapter below scans through THIS one:
|
|
759
|
+
# the eight tool-call contexts live in \`ai-harness\`, not in \`ai-scanner\`
|
|
760
|
+
# (which is the periodic endpoint-inventory collector). Two names, never one
|
|
761
|
+
# shared override — pointing a single variable at one product would leave the
|
|
762
|
+
# other silently addressing a file that is not it.
|
|
763
|
+
export AI_HARNESS_BIN="\${AI_HARNESS_BIN:-$HOME/${agentDir}/bin/ai-harness}"
|
|
554
764
|
export AI_SCANNER_ACCESS_KEY="${accessKey}"
|
|
555
765
|
export AI_SCANNER_LEVEL="\${AI_SCANNER_LEVEL:-high}"
|
|
556
766
|
export AI_SCANNER_EXCLUDE_DIRS="\${AI_SCANNER_EXCLUDE_DIRS:-}"
|
|
@@ -569,11 +779,11 @@ ${UNSCANNED_NOTE}
|
|
|
569
779
|
|
|
570
780
|
run_scanner() {
|
|
571
781
|
if command -v timeout >/dev/null 2>&1; then
|
|
572
|
-
timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${
|
|
782
|
+
timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_HARNESS_BIN}" "$@"
|
|
573
783
|
elif command -v gtimeout >/dev/null 2>&1; then
|
|
574
|
-
gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${
|
|
784
|
+
gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_HARNESS_BIN}" "$@"
|
|
575
785
|
else
|
|
576
|
-
"\${
|
|
786
|
+
"\${AI_HARNESS_BIN}" "$@"
|
|
577
787
|
fi
|
|
578
788
|
local rc=$?
|
|
579
789
|
[ $rc -eq 124 ] && return 0
|
|
@@ -593,7 +803,7 @@ source "$SCRIPT_DIR/env.sh"
|
|
|
593
803
|
cat > /dev/null
|
|
594
804
|
bash "$SCRIPT_DIR/start_scanner.sh" 2>/dev/null || true
|
|
595
805
|
cat <<EOF
|
|
596
|
-
{"env":{"AI_SCANNER_BIN":"\${AI_SCANNER_BIN}","AI_SCANNER_ACCESS_KEY":"${accessKey}","AI_SCANNER_LEVEL":"high"}}
|
|
806
|
+
{"env":{"AI_SCANNER_BIN":"\${AI_SCANNER_BIN}","AI_HARNESS_BIN":"\${AI_HARNESS_BIN}","AI_SCANNER_ACCESS_KEY":"${accessKey}","AI_SCANNER_LEVEL":"high"}}
|
|
597
807
|
EOF
|
|
598
808
|
`;
|
|
599
809
|
}
|
|
@@ -601,7 +811,7 @@ EOF
|
|
|
601
811
|
export const CURSOR_PRE_SHELL_SH = `#!/bin/bash
|
|
602
812
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
603
813
|
source "$SCRIPT_DIR/env.sh"
|
|
604
|
-
BIN="$
|
|
814
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
605
815
|
export AI_SCANNER_DETECTION_LOG
|
|
606
816
|
jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
607
817
|
`;
|
|
@@ -609,7 +819,7 @@ jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
|
609
819
|
export const CURSOR_PRE_READ_SH = `#!/bin/bash
|
|
610
820
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
611
821
|
source "$SCRIPT_DIR/env.sh"
|
|
612
|
-
BIN="$
|
|
822
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
613
823
|
export AI_SCANNER_DETECTION_LOG
|
|
614
824
|
FILE_PATH=$(jq -r '.tool_input.file_path // ""')
|
|
615
825
|
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
@@ -619,7 +829,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
|
619
829
|
export const CURSOR_PRE_EDIT_SH = `#!/bin/bash
|
|
620
830
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
621
831
|
source "$SCRIPT_DIR/env.sh"
|
|
622
|
-
BIN="$
|
|
832
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
623
833
|
export AI_SCANNER_DETECTION_LOG
|
|
624
834
|
INPUT=$(cat)
|
|
625
835
|
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
@@ -636,7 +846,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
|
636
846
|
export const CURSOR_POST_SHELL_SH = `#!/bin/bash
|
|
637
847
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
638
848
|
source "$SCRIPT_DIR/env.sh"
|
|
639
|
-
BIN="$
|
|
849
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
640
850
|
export AI_SCANNER_DETECTION_LOG
|
|
641
851
|
INPUT=$(cat)
|
|
642
852
|
COMMAND=$(jq -r '.tool_output // ""' <<<"$INPUT" | head -1 && jq -r '.tool_input.command // ""' <<<"$INPUT")
|
|
@@ -646,7 +856,7 @@ jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan bash-output --command
|
|
|
646
856
|
export const CURSOR_POST_READ_SH = `#!/bin/bash
|
|
647
857
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
648
858
|
source "$SCRIPT_DIR/env.sh"
|
|
649
|
-
BIN="$
|
|
859
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
650
860
|
export AI_SCANNER_DETECTION_LOG
|
|
651
861
|
INPUT=$(cat)
|
|
652
862
|
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
@@ -657,7 +867,7 @@ jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan read-output --file "$F
|
|
|
657
867
|
export const CURSOR_POST_WRITE_SH = `#!/bin/bash
|
|
658
868
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
659
869
|
source "$SCRIPT_DIR/env.sh"
|
|
660
|
-
BIN="$
|
|
870
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
661
871
|
export AI_SCANNER_DETECTION_LOG
|
|
662
872
|
INPUT=$(cat)
|
|
663
873
|
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
@@ -672,7 +882,7 @@ run_scanner scan edit-output --file "$FILE_PATH" < "$FILE_PATH"
|
|
|
672
882
|
export const WINDSURF_ENSURE_SCANNER_SH = `#!/bin/bash
|
|
673
883
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
674
884
|
source "$SCRIPT_DIR/env.sh"
|
|
675
|
-
BIN="$
|
|
885
|
+
BIN="$AI_HARNESS_BIN"
|
|
676
886
|
LOCK_FILE="/tmp/.ai-scanner-ensure.lock"
|
|
677
887
|
if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
|
|
678
888
|
return 0 2>/dev/null || true
|
|
@@ -688,7 +898,7 @@ return 0 2>/dev/null || true
|
|
|
688
898
|
export const WINDSURF_PRE_COMMAND_SH = `#!/bin/bash
|
|
689
899
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
690
900
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
691
|
-
BIN="$
|
|
901
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
692
902
|
export AI_SCANNER_DETECTION_LOG
|
|
693
903
|
CONTENT=$(jq -r '.tool_info.command_line // ""')
|
|
694
904
|
[ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
|
|
@@ -698,7 +908,7 @@ printf '%s' "$CONTENT" | run_scanner scan bash-input
|
|
|
698
908
|
export const WINDSURF_PRE_READ_SH = `#!/bin/bash
|
|
699
909
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
700
910
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
701
|
-
BIN="$
|
|
911
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
702
912
|
export AI_SCANNER_DETECTION_LOG
|
|
703
913
|
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
704
914
|
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
@@ -708,7 +918,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
|
708
918
|
export const WINDSURF_PRE_WRITE_SH = `#!/bin/bash
|
|
709
919
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
710
920
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
711
|
-
BIN="$
|
|
921
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
712
922
|
export AI_SCANNER_DETECTION_LOG
|
|
713
923
|
INPUT=$(cat)
|
|
714
924
|
FILE_PATH=$(jq -r '.tool_info.file_path // ""' <<<"$INPUT")
|
|
@@ -720,7 +930,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
|
720
930
|
export const WINDSURF_POST_READ_SH = `#!/bin/bash
|
|
721
931
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
722
932
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
723
|
-
BIN="$
|
|
933
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
724
934
|
export AI_SCANNER_DETECTION_LOG
|
|
725
935
|
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
726
936
|
[ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
|
|
@@ -730,7 +940,7 @@ run_scanner scan read-output --file "$FILE_PATH" < "$FILE_PATH"
|
|
|
730
940
|
export const WINDSURF_POST_WRITE_SH = `#!/bin/bash
|
|
731
941
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
732
942
|
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
733
|
-
BIN="$
|
|
943
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
734
944
|
export AI_SCANNER_DETECTION_LOG
|
|
735
945
|
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
736
946
|
[ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
|
|
@@ -773,7 +983,7 @@ exec "$SCRIPT_DIR/start_scanner.sh"
|
|
|
773
983
|
export const KIRO_PRE_SHELL_SH = `#!/bin/bash
|
|
774
984
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
775
985
|
source "$SCRIPT_DIR/env.sh"
|
|
776
|
-
BIN="$
|
|
986
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
777
987
|
export AI_SCANNER_DETECTION_LOG
|
|
778
988
|
jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
779
989
|
`;
|
|
@@ -781,7 +991,7 @@ jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
|
781
991
|
export const KIRO_PRE_READ_SH = `#!/bin/bash
|
|
782
992
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
783
993
|
source "$SCRIPT_DIR/env.sh"
|
|
784
|
-
BIN="$
|
|
994
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
785
995
|
export AI_SCANNER_DETECTION_LOG
|
|
786
996
|
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""')
|
|
787
997
|
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
@@ -791,7 +1001,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
|
791
1001
|
export const KIRO_PRE_WRITE_SH = `#!/bin/bash
|
|
792
1002
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
793
1003
|
source "$SCRIPT_DIR/env.sh"
|
|
794
|
-
BIN="$
|
|
1004
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
795
1005
|
export AI_SCANNER_DETECTION_LOG
|
|
796
1006
|
INPUT=$(cat)
|
|
797
1007
|
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
|
|
@@ -803,7 +1013,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
|
803
1013
|
export const KIRO_POST_SHELL_SH = `#!/bin/bash
|
|
804
1014
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
805
1015
|
source "$SCRIPT_DIR/env.sh"
|
|
806
|
-
BIN="$
|
|
1016
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
807
1017
|
export AI_SCANNER_DETECTION_LOG
|
|
808
1018
|
INPUT=$(cat)
|
|
809
1019
|
COMMAND=$(jq -r '.tool_input.command // ""' <<<"$INPUT")
|
|
@@ -814,7 +1024,7 @@ printf '%s' "$RESPONSE" | run_scanner scan bash-output --command "$COMMAND"
|
|
|
814
1024
|
export const KIRO_POST_READ_SH = `#!/bin/bash
|
|
815
1025
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
816
1026
|
source "$SCRIPT_DIR/env.sh"
|
|
817
|
-
BIN="$
|
|
1027
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
818
1028
|
export AI_SCANNER_DETECTION_LOG
|
|
819
1029
|
INPUT=$(cat)
|
|
820
1030
|
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""' <<<"$INPUT")
|
|
@@ -826,7 +1036,7 @@ printf '%s' "$RESPONSE" | run_scanner scan read-output --file "$FILE_PATH"
|
|
|
826
1036
|
export const KIRO_POST_WRITE_SH = `#!/bin/bash
|
|
827
1037
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
828
1038
|
source "$SCRIPT_DIR/env.sh"
|
|
829
|
-
BIN="$
|
|
1039
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
830
1040
|
export AI_SCANNER_DETECTION_LOG
|
|
831
1041
|
INPUT=$(cat)
|
|
832
1042
|
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
|
|
@@ -902,7 +1112,7 @@ export const CODEX_SESSION_INIT_SH = `#!/bin/bash
|
|
|
902
1112
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
903
1113
|
source "$SCRIPT_DIR/env.sh"
|
|
904
1114
|
cat > /dev/null # drain stdin so Codex doesn't block on a write
|
|
905
|
-
BIN="$
|
|
1115
|
+
BIN="$AI_HARNESS_BIN"
|
|
906
1116
|
LOCK_FILE="/tmp/.ai-scanner-codex-init.lock"
|
|
907
1117
|
if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
|
|
908
1118
|
exit 0
|
|
@@ -919,7 +1129,7 @@ export const CODEX_PRE_SHELL_SH = `#!/bin/bash
|
|
|
919
1129
|
# Codex PreToolUse Bash hook: scan command and return block JSON to Codex.
|
|
920
1130
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
921
1131
|
source "$SCRIPT_DIR/env.sh"
|
|
922
|
-
BIN="$
|
|
1132
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
923
1133
|
export AI_SCANNER_DETECTION_LOG
|
|
924
1134
|
INPUT=$(cat)
|
|
925
1135
|
CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
|
|
@@ -949,7 +1159,7 @@ export const CODEX_POST_SHELL_SH = `#!/bin/bash
|
|
|
949
1159
|
# Codex PostToolUse Bash hook: scan command output and return feedback JSON.
|
|
950
1160
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
951
1161
|
source "$SCRIPT_DIR/env.sh"
|
|
952
|
-
BIN="$
|
|
1162
|
+
BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
|
|
953
1163
|
export AI_SCANNER_DETECTION_LOG
|
|
954
1164
|
INPUT=$(cat)
|
|
955
1165
|
CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
|
package/src/toml-merge.mjs
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
// Line-based TOML merger for Codex's ~/.codex/config.toml feature flag.
|
|
2
|
-
//
|
|
2
|
+
//
|
|
3
|
+
// Codex requires `[features] hooks = true` to activate hook dispatch. The older
|
|
4
|
+
// spelling `[features] codex_hooks = true` is a **deprecated alias**: Codex still
|
|
5
|
+
// honours it, but prints
|
|
6
|
+
//
|
|
7
|
+
// ⚠ `[features].codex_hooks` is deprecated. Use `[features].hooks` instead.
|
|
8
|
+
//
|
|
9
|
+
// on every start (issue #2367). So we write the canonical key and *migrate* any
|
|
10
|
+
// legacy key we find, instead of leaving both around.
|
|
11
|
+
//
|
|
3
12
|
// Users may already have their own config (model settings, other features), so
|
|
4
13
|
// we must merge rather than overwrite.
|
|
5
14
|
//
|
|
6
|
-
// Scope: *only* handles a single
|
|
7
|
-
// parser, no external deps. Preserves comments, blank lines, ordering, and
|
|
15
|
+
// Scope: *only* handles a single logical flag inside a single section. No full
|
|
16
|
+
// TOML parser, no external deps. Preserves comments, blank lines, ordering, and
|
|
8
17
|
// other sections exactly.
|
|
18
|
+
//
|
|
19
|
+
// ── Migration decision table (merge) ─────────────────────────────────
|
|
20
|
+
// Only the *first* `[features]` section is considered (duplicate table headers
|
|
21
|
+
// are invalid TOML anyway — see the known limitation at the bottom of this file).
|
|
22
|
+
//
|
|
23
|
+
// old key | new key | result
|
|
24
|
+
// --------+---------+--------------------------------------------------------
|
|
25
|
+
// absent | absent | insert `hooks = true` right under the section header
|
|
26
|
+
// absent | true | no-op (byte-identical output)
|
|
27
|
+
// absent | false | value flipped to true, line position/comment preserved
|
|
28
|
+
// true | absent | line RENAMED in place -> `hooks = true`
|
|
29
|
+
// (indentation, position and trailing comment preserved)
|
|
30
|
+
// false | absent | same rename, and the value is set to true
|
|
31
|
+
// any | any | the canonical `hooks` line wins and is set to true;
|
|
32
|
+
// the deprecated `codex_hooks` line is REMOVED entirely.
|
|
33
|
+
// This is the only case where a trailing comment can be
|
|
34
|
+
// lost, because it is attached to the line we delete.
|
|
35
|
+
// Conflicting values need no tie-break: setup means
|
|
36
|
+
// "hooks on", so the result is always `hooks = true`.
|
|
37
|
+
//
|
|
38
|
+
// Standalone comment lines are never deleted, even when they sit directly above
|
|
39
|
+
// a removed legacy key — deleting prose a user wrote is worse than leaving it.
|
|
40
|
+
//
|
|
41
|
+
// ── unmerge ──────────────────────────────────────────────────────────
|
|
42
|
+
// Strips BOTH spellings, so a config written by an older aidr (legacy key) is
|
|
43
|
+
// fully cleaned up by a newer aidr's uninstall. Everything else in `[features]`
|
|
44
|
+
// (the user's own flags and comments) survives; the section header is dropped
|
|
45
|
+
// only when nothing but blanks/comments would remain.
|
|
9
46
|
|
|
10
47
|
const SECTION = "features";
|
|
11
|
-
|
|
48
|
+
// Canonical key. Codex >= the version that introduced `[features].hooks`.
|
|
49
|
+
export const FEATURE_KEY = "hooks";
|
|
50
|
+
// Deprecated alias still accepted by Codex, but it prints a warning (#2367).
|
|
51
|
+
export const LEGACY_FEATURE_KEY = "codex_hooks";
|
|
12
52
|
const VALUE = "true";
|
|
13
53
|
|
|
14
54
|
// Match a section header line: optional leading whitespace, `[name]`, optional trailing comment.
|
|
@@ -20,10 +60,22 @@ function sectionHeader(name) {
|
|
|
20
60
|
// Match any section header (used to find section boundaries).
|
|
21
61
|
const ANY_SECTION_RE = /^\s*\[\s*[^\]]+\s*\](?:\s*#.*)?$/;
|
|
22
62
|
|
|
23
|
-
// Match `key = value` with optional trailing comment
|
|
63
|
+
// Match `key = value` with optional trailing comment.
|
|
64
|
+
// Groups: 1 = indent, 2 = key, 3 = ` = `, 4 = value token, 5 = trailing comment.
|
|
65
|
+
// The `^\s*` anchor means the `hooks` matcher can never hit a `codex_hooks`
|
|
66
|
+
// line (and vice versa) — the key must start the line after whitespace only.
|
|
24
67
|
function keyAssignment(key) {
|
|
25
68
|
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
-
return new RegExp(`^(\\s
|
|
69
|
+
return new RegExp(`^(\\s*)(${escaped})(\\s*=\\s*)([^#\\n]*?)(\\s*(?:#.*)?)$`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const CANONICAL_RE = keyAssignment(FEATURE_KEY);
|
|
73
|
+
const LEGACY_RE = keyAssignment(LEGACY_FEATURE_KEY);
|
|
74
|
+
|
|
75
|
+
// Rewrite a matched assignment line as `<indent>hooks<eq>true<trailing>`.
|
|
76
|
+
function rewriteAsEnabled(line, re) {
|
|
77
|
+
const m = re.exec(line);
|
|
78
|
+
return `${m[1]}${FEATURE_KEY}${m[3]}${VALUE}${m[5]}`;
|
|
27
79
|
}
|
|
28
80
|
|
|
29
81
|
// Detect obvious malformed TOML so we fail closed instead of silently corrupting.
|
|
@@ -61,37 +113,64 @@ function findSectionRange(lines, section) {
|
|
|
61
113
|
return { start, end };
|
|
62
114
|
}
|
|
63
115
|
|
|
64
|
-
//
|
|
65
|
-
|
|
116
|
+
// Locate the first canonical / first legacy assignment inside a section range.
|
|
117
|
+
function findKeys(lines, range) {
|
|
118
|
+
let canonical = -1;
|
|
119
|
+
let legacy = -1;
|
|
120
|
+
for (let i = range.start + 1; i < range.end; i++) {
|
|
121
|
+
if (canonical === -1 && CANONICAL_RE.test(lines[i])) canonical = i;
|
|
122
|
+
if (legacy === -1 && LEGACY_RE.test(lines[i])) legacy = i;
|
|
123
|
+
}
|
|
124
|
+
return { canonical, legacy };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Merge `[features] hooks = true` into the given TOML text, migrating the
|
|
128
|
+
// deprecated `codex_hooks` spelling and preserving everything else.
|
|
129
|
+
// Idempotent: re-running on an already-merged file returns the input verbatim.
|
|
66
130
|
export function mergeCodexHookFeature(text) {
|
|
67
131
|
const input = text ?? "";
|
|
68
132
|
if (input !== "") validate(input);
|
|
69
133
|
|
|
70
134
|
// Empty file / whitespace-only: write a fresh minimal TOML.
|
|
71
135
|
if (input.trim() === "") {
|
|
72
|
-
return `[${SECTION}]\n${
|
|
136
|
+
return `[${SECTION}]\n${FEATURE_KEY} = ${VALUE}\n`;
|
|
73
137
|
}
|
|
74
138
|
|
|
75
139
|
const lines = input.split("\n");
|
|
76
140
|
// Preserve final newline state — JSON.stringify-style: if original ended with
|
|
77
141
|
// "\n", .split gives a trailing "" element we'll keep and rejoin with.
|
|
78
142
|
const range = findSectionRange(lines, SECTION);
|
|
79
|
-
const assignRe = keyAssignment(KEY);
|
|
80
143
|
|
|
81
144
|
if (range) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
145
|
+
const { canonical, legacy } = findKeys(lines, range);
|
|
146
|
+
|
|
147
|
+
if (canonical !== -1 && legacy !== -1) {
|
|
148
|
+
// Both spellings present. The canonical line wins (position + comment
|
|
149
|
+
// kept); the deprecated alias line goes away, which is what silences the
|
|
150
|
+
// warning. Conflicting values resolve to `true` because that is what this
|
|
151
|
+
// setup means.
|
|
152
|
+
lines[canonical] = rewriteAsEnabled(lines[canonical], CANONICAL_RE);
|
|
153
|
+
lines.splice(legacy, 1);
|
|
154
|
+
return lines.join("\n");
|
|
91
155
|
}
|
|
156
|
+
|
|
157
|
+
if (legacy !== -1) {
|
|
158
|
+
// Legacy only — rename in place so ordering, indentation and any trailing
|
|
159
|
+
// comment survive. `codex_hooks = false` becomes `hooks = true`.
|
|
160
|
+
lines[legacy] = rewriteAsEnabled(lines[legacy], LEGACY_RE);
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (canonical !== -1) {
|
|
165
|
+
const m = CANONICAL_RE.exec(lines[canonical]);
|
|
166
|
+
if (m[4].trim() === VALUE) return input; // already correct, no-op
|
|
167
|
+
lines[canonical] = rewriteAsEnabled(lines[canonical], CANONICAL_RE);
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
92
171
|
// Key absent — insert right after the section header (before any subkeys),
|
|
93
172
|
// so the new line doesn't get buried under a long section.
|
|
94
|
-
lines.splice(range.start + 1, 0, `${
|
|
173
|
+
lines.splice(range.start + 1, 0, `${FEATURE_KEY} = ${VALUE}`);
|
|
95
174
|
return lines.join("\n");
|
|
96
175
|
}
|
|
97
176
|
|
|
@@ -100,12 +179,14 @@ export function mergeCodexHookFeature(text) {
|
|
|
100
179
|
const needsLeadingBlank = input.length > 0 && !input.endsWith("\n\n");
|
|
101
180
|
const prefix = input.endsWith("\n") ? "" : "\n";
|
|
102
181
|
const spacer = needsLeadingBlank ? "\n" : "";
|
|
103
|
-
return `${input}${prefix}${spacer}[${SECTION}]\n${
|
|
182
|
+
return `${input}${prefix}${spacer}[${SECTION}]\n${FEATURE_KEY} = ${VALUE}\n`;
|
|
104
183
|
}
|
|
105
184
|
|
|
106
|
-
// Reverse of mergeCodexHookFeature: strip our
|
|
107
|
-
// - Removes the
|
|
108
|
-
//
|
|
185
|
+
// Reverse of mergeCodexHookFeature: strip our feature flag line(s).
|
|
186
|
+
// - Removes `hooks` AND the deprecated `codex_hooks` (a config written by an
|
|
187
|
+
// older aidr carries the legacy spelling; leaving it behind would keep the
|
|
188
|
+
// deprecation warning alive after uninstall).
|
|
189
|
+
// - Removes an empty `[features]` section if our line(s) were the only content.
|
|
109
190
|
// Returns { text, empty } — empty=true means the TOML is now effectively blank
|
|
110
191
|
// so the caller can delete the file entirely.
|
|
111
192
|
export function unmergeCodexHookFeature(text) {
|
|
@@ -120,17 +201,17 @@ export function unmergeCodexHookFeature(text) {
|
|
|
120
201
|
return { text: input, empty: input.trim() === "" };
|
|
121
202
|
}
|
|
122
203
|
|
|
123
|
-
const
|
|
124
|
-
let removedIdx = -1;
|
|
204
|
+
const removed = [];
|
|
125
205
|
for (let i = range.start + 1; i < range.end; i++) {
|
|
126
|
-
if (
|
|
206
|
+
if (CANONICAL_RE.test(lines[i]) || LEGACY_RE.test(lines[i])) removed.push(i);
|
|
127
207
|
}
|
|
128
208
|
|
|
129
|
-
if (
|
|
209
|
+
if (removed.length === 0) {
|
|
130
210
|
return { text: input, empty: input.trim() === "" };
|
|
131
211
|
}
|
|
132
212
|
|
|
133
|
-
|
|
213
|
+
// Delete back-to-front so earlier indices stay valid.
|
|
214
|
+
for (let k = removed.length - 1; k >= 0; k--) lines.splice(removed[k], 1);
|
|
134
215
|
|
|
135
216
|
// Re-evaluate the section bounds after removal.
|
|
136
217
|
const newRange = findSectionRange(lines, SECTION);
|
|
@@ -165,3 +246,10 @@ export function unmergeCodexHookFeature(text) {
|
|
|
165
246
|
const result = lines.join("\n");
|
|
166
247
|
return { text: result, empty: result.trim() === "" };
|
|
167
248
|
}
|
|
249
|
+
|
|
250
|
+
// ── Known limitation (declared, not fixed) ───────────────────────────
|
|
251
|
+
// Only the FIRST `[features]` table is inspected. A file with two `[features]`
|
|
252
|
+
// headers is already invalid TOML (duplicate table), so Codex would reject it
|
|
253
|
+
// before our flag mattered; handling it here would mean writing a real parser.
|
|
254
|
+
// If such a file ever appears, a legacy key living in the second block would
|
|
255
|
+
// survive the migration and the deprecation warning would persist.
|