@dsh-fly/dsh-plugin-fly-mode 0.1.8 → 0.1.10
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/README.md +3 -3
- package/cordis.patch.yml +0 -1
- package/package.json +2 -2
- package/tools/dsh-plugin-fly-artifact/index.js +21 -2
- package/tools/dsh-plugin-fly-binary/index.js +110 -22
- package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/SKILL.md +3 -0
- package/tools/dsh-plugin-fly-blackbox/index.js +490 -5
- package/tools/dsh-plugin-fly-diag/index.js +177 -28
- package/tools/dsh-plugin-fly-pwn/index.js +91 -9
- package/tools/dsh-plugin-fly-recon/index.js +117 -16
- package/tools/dsh-plugin-fly-reverse/index.js +132 -18
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
- **Fly 标准模式** `preset-fly-standard`(行表 = **上游 `standard` 的行表** + 一行 smart,33 行)——
|
|
12
12
|
由生成器从**引擎自己的组合结果**里取上游行表(不手抄),所以引擎升级后行表跟着走、漂了 `--check` 会红。
|
|
13
13
|
其中
|
|
14
|
-
**
|
|
15
|
-
`tools/`**,行表用**本包子路径**引用它们 —— 装这个包**不会**在 npm 上再拉这
|
|
14
|
+
**8 个 Fly 工具**(binary / reverse / pwn / diag / recon / blackbox / artifact / guard)的源码**内联在包内
|
|
15
|
+
`tools/`**,行表用**本包子路径**引用它们 —— 装这个包**不会**在 npm 上再拉这 8 个包;
|
|
16
16
|
**`fly-smart` 单独一个 plugin**:它是本包 `dependencies` 里**唯一**一项,行表保持裸名,
|
|
17
17
|
由 npm 装进 profile 的 `node_modules`(它变化最快,且与工作台面板是一个整体,不适合内联)。
|
|
18
18
|
- **不是**:不带 `dsh.client`(模式是 agent 平面,没有浏览器半边);不带 Fly 工作台
|
|
@@ -76,7 +76,7 @@ dsh plugin --profile=web add @dsh-fly/dsh-plugin-fly-mode@0.1.3 --registry=https
|
|
|
76
76
|
|
|
77
77
|
## 仓库状态(2026-10-11)
|
|
78
78
|
|
|
79
|
-
- 形状定稿:一条 `- insert:` 声明 + 包内 `tools/`(
|
|
79
|
+
- 形状定稿:一条 `- insert:` 声明 + 包内 `tools/`(8 个工具内联,行表用本包子路径引用)+
|
|
80
80
|
`dsh-plugin-fly-smart` 作为**唯一** npm 依赖。声明由 `dsh-fly` 仓的
|
|
81
81
|
`scripts/build-fly-mode-bundle.mjs` 生成,门禁用 `--check` 断漂移(含多出来的文件)。
|
|
82
82
|
- `.github/workflows/{ci,release}.yml` **已适配本仓**(纯声明包:不装依赖、直接跑
|
package/cordis.patch.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-fly/dsh-plugin-fly-mode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Fly 模式作为一个可安装的包:**两个 preset** —— Fly 安全模式(preset-fly,
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"description": "Fly 模式作为一个可安装的包:**两个 preset** —— Fly 安全模式(preset-fly,8 个安全工具内联 + smart)与 Fly 标准模式(preset-fly-standard,上游 standard 的行表 + smart,无安全工具);依赖单独发布的 dsh-plugin-fly-smart。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -39,6 +39,12 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
39
39
|
*
|
|
40
40
|
* 两个入口都在 P1 的"结论要能回指源码位置"那条要求下:清单的每条成员给**相对路径**,
|
|
41
41
|
* 边界的每条命中给 **file:line**。
|
|
42
|
+
*
|
|
43
|
+
* **起因**(为什么这个包存在):`morluto/rea` 那边有 APK/JADX、固件/Binwalk、.NET、**JS/ASAR 与 HAR**
|
|
44
|
+
* 这些"看得懂打包产物"的能力,而 fly 这边此前只有原生二进制三件套 —— 一个 Electron 应用的
|
|
45
|
+
* 150MB `app.asar` 摆在面前,只能靠 `bin_strings` 碰运气。安全模式的定位是客户端黑盒排查,
|
|
46
|
+
* 所以这一面必须在拆仓之前补上(规划 §3.2):先把"清单 + 字面量边界"两层做出来,
|
|
47
|
+
* 再谈 AST 与 CDP(P2)。**不抄代码,只在设计上对齐**(分层、覆盖率、bounds 三件)。
|
|
42
48
|
*/
|
|
43
49
|
|
|
44
50
|
export const name = 'fly-artifact'
|
|
@@ -59,6 +65,19 @@ function shq(value) {
|
|
|
59
65
|
* version. `scripts/verify-security-tools.mjs` now fails loudly if a call site
|
|
60
66
|
* names a seam method the executor does not have.
|
|
61
67
|
*/
|
|
68
|
+
/**
|
|
69
|
+
* 路径参数规范化:以 `-` 开头的路径加 `./` 前缀(2026-10-13)。
|
|
70
|
+
*
|
|
71
|
+
* 为什么不插 `--`:实测本机(macOS,发布目标平台)**objdump(-h/-d/-T)/ base64 -i / otool -hv 都不支持
|
|
72
|
+
* `--`**,插进去直接失败;而 `file -b --version` 会打印 `file-5.41`、`nm -u --version` 会打印
|
|
73
|
+
* `llvm-nm, …`,两者都 **exit 0** ⇒ 工具会把版本串当**数据**返回。加 `./` 之后它就是一个普通相对路径:
|
|
74
|
+
* 不存在就正常报错,存在就是那个文件。
|
|
75
|
+
*/
|
|
76
|
+
function safePath(value) {
|
|
77
|
+
const text = String(value ?? '')
|
|
78
|
+
return text.startsWith('-') ? './' + text : text
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
async function runCmd(shell, argv, exec, opts = {}) {
|
|
63
82
|
const result = await (await shell.execute(shell.resolve({
|
|
64
83
|
command: argv.map(shq).join(' '),
|
|
@@ -632,7 +651,7 @@ export function apply(ctx) {
|
|
|
632
651
|
},
|
|
633
652
|
output: stringOutput,
|
|
634
653
|
async execute(args, exec) {
|
|
635
|
-
const path = typeof args.path === 'string' ? args.path.trim() : ''
|
|
654
|
+
const path = safePath(typeof args.path === 'string' ? args.path.trim() : '')
|
|
636
655
|
if (path === '') throw new Error('artifact_asar_inventory 需要 path(.asar 的路径)')
|
|
637
656
|
const top = readTop(args.top, 'artifact_asar_inventory')
|
|
638
657
|
|
|
@@ -770,7 +789,7 @@ export function apply(ctx) {
|
|
|
770
789
|
},
|
|
771
790
|
output: stringOutput,
|
|
772
791
|
async execute(args, exec) {
|
|
773
|
-
const path = typeof args.path === 'string' ? args.path.trim() : ''
|
|
792
|
+
const path = safePath(typeof args.path === 'string' ? args.path.trim() : '')
|
|
774
793
|
if (path === '') throw new Error('artifact_js_boundaries 需要 path(.asar 的路径,或解包后的 JS 目录)')
|
|
775
794
|
const top = readTop(args.top, 'artifact_js_boundaries')
|
|
776
795
|
const scope = args.scope === undefined ? 'app' : args.scope
|
|
@@ -5,6 +5,14 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
5
5
|
* Registers model-facing analysis tools on ctx.tools. All command execution
|
|
6
6
|
* goes through the host `shell` capability seam so the sandbox confines it;
|
|
7
7
|
* a tool never shells out via node:child_process directly.
|
|
8
|
+
*
|
|
9
|
+
* **失败面的口径是"大声"**(Q5):缺 `readelf`/`objdump`/`strings`/`file`/`wc` 时**不静默降级** ——
|
|
10
|
+
* · 能在仓库内补的就补:没有 GNU readelf 就用**仓库内置的原生解析器**算头部/段/动态(并在
|
|
11
|
+
* 结果的 `limitations` 里明说"这是内置解析器算的,不是 readelf 的输出");
|
|
12
|
+
* · 补不了的就**抛**:`requireOk` 让缺命令以 `CommandError` 报出来(历史教训:`pwn_checksec`
|
|
13
|
+
* 曾在缺工具时静默回退,对**任何**二进制都报 `pie=no`);
|
|
14
|
+
* · "没有"要**说成没有**:`bin_strings` 一条都没匹配时返回"no printable strings matching …",
|
|
15
|
+
* 绝不返回空串(空串会被读成"这个文件没有字符串");节表读不到时同理。
|
|
8
16
|
*/
|
|
9
17
|
export const name = 'fly-binary'
|
|
10
18
|
export const inject = ['tools', 'shell']
|
|
@@ -24,6 +32,19 @@ function shq(value) {
|
|
|
24
32
|
* version. `scripts/verify-security-tools.mjs` now fails loudly if a call site
|
|
25
33
|
* names a seam method the executor does not have.
|
|
26
34
|
*/
|
|
35
|
+
/**
|
|
36
|
+
* 路径参数规范化:以 `-` 开头的路径加 `./` 前缀(2026-10-13)。
|
|
37
|
+
*
|
|
38
|
+
* 为什么不插 `--`:实测本机(macOS,发布目标平台)**objdump(-h/-d/-T)/ base64 -i / otool -hv 都不支持
|
|
39
|
+
* `--`**,插进去直接失败;而 `file -b --version` 会打印 `file-5.41`、`nm -u --version` 会打印
|
|
40
|
+
* `llvm-nm, …`,两者都 **exit 0** ⇒ 工具会把版本串当**数据**返回(`bin_sections { path: '--version' }`
|
|
41
|
+
* 真发生过)。加 `./` 之后它就是一个普通相对路径:不存在就正常报错,存在就是那个文件。
|
|
42
|
+
*/
|
|
43
|
+
function safePath(value) {
|
|
44
|
+
const text = String(value ?? '')
|
|
45
|
+
return text.startsWith('-') ? './' + text : text
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
async function runCmd(shell, argv, exec, opts = {}) {
|
|
28
49
|
const result = await (await shell.execute(shell.resolve({
|
|
29
50
|
command: argv.map(shq).join(' '),
|
|
@@ -54,9 +75,19 @@ async function runCmd(shell, argv, exec, opts = {}) {
|
|
|
54
75
|
*/
|
|
55
76
|
function requireOk(run, what) {
|
|
56
77
|
if (run.exitCode === 0) return run.stdout
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
// 沙箱拒绝的形状是**结果正文里的标记**(`[sandbox: file access denied under <mode> mode]`,
|
|
79
|
+
// 抄自 @deepseek-ai/dsh-sandbox/index.js:65)—— 它落在 stdout 上;只读 stderr 就会把它丢掉,
|
|
80
|
+
// 于是模型读到的是「命令失败」,而不是「你没有被授权」。所以标记必须原样带进错误文案。
|
|
81
|
+
const sandboxMark = /^\[sandbox: .*\]$/mu.exec(run.stdout)
|
|
82
|
+
const detail = [sandboxMark?.[0], ...run.stderr.trim().split('\n').map(line => line.trim()).filter(Boolean)].slice(0, 3).join(' | ')
|
|
83
|
+
const notInstalled = run.exitCode === 127 || /command not found/iu.test(run.stderr)
|
|
84
|
+
const missing = notInstalled ? ' — the command is not installed on this host' : ''
|
|
85
|
+
// 失败文案要对**读它的模型**可行动(Q6):不是裸 stderr,但也不许臆断原因 ——
|
|
86
|
+
// 只给「这条 exit 属于哪一类」与「下一步可以试什么」,具体判断仍留给读的人。
|
|
87
|
+
const next = notInstalled
|
|
88
|
+
? 'next: this host lacks that command — look for a built-in fallback in this package, or read the file another way'
|
|
89
|
+
: 'next: check the path, and that the file is what this tool expects (bin_file shows what it is); the line above is the command own reason'
|
|
90
|
+
const error = new Error(`${what} failed (exit ${run.exitCode})${missing}: ${run.command}${detail === '' ? '' : `\n${detail}\n${next}`}`)
|
|
60
91
|
error.name = 'CommandError'
|
|
61
92
|
throw error
|
|
62
93
|
}
|
|
@@ -432,10 +463,23 @@ export function apply(ctx) {
|
|
|
432
463
|
render: (_args, value) => [{ type: 'text', text: value }],
|
|
433
464
|
},
|
|
434
465
|
async execute(args, exec) {
|
|
435
|
-
const
|
|
466
|
+
const path = safePath(args.path)
|
|
467
|
+
const wc = await runCmd(shell, ['wc', '-c', path], exec)
|
|
436
468
|
const size = requireOk(wc, 'wc -c').trim().split(/\s+/)[0] ?? '0'
|
|
437
|
-
const identify = await runCmd(shell, ['file', '-b',
|
|
438
|
-
|
|
469
|
+
const identify = await runCmd(shell, ['file', '-b', path], exec)
|
|
470
|
+
// 证据形状(规划 §5.1):信封 JSON,原文本原样进 observed.dump。
|
|
471
|
+
const text = `size=${size}\n${requireOk(identify, 'file -b').trim()}`
|
|
472
|
+
return JSON.stringify({
|
|
473
|
+
tool: 'bin_file',
|
|
474
|
+
target: args.path,
|
|
475
|
+
operation: 'one wc -c + one file -b (no parsing of the file content)',
|
|
476
|
+
observed: { bytes: Number(size), dump: text },
|
|
477
|
+
confidence: 'observed',
|
|
478
|
+
limitations: [
|
|
479
|
+
'只报 wc -c 的字节数与 file(1) 的判断:**不解析文件内部**(要字段另用 bin_readelf / bin_sections)',
|
|
480
|
+
'file(1) 的判断来自魔数与片段匹配:损坏或被裁剪的文件也可能被认成某种格式',
|
|
481
|
+
],
|
|
482
|
+
}, null, 2)
|
|
439
483
|
},
|
|
440
484
|
}))
|
|
441
485
|
|
|
@@ -453,18 +497,39 @@ export function apply(ctx) {
|
|
|
453
497
|
},
|
|
454
498
|
async execute(args, exec) {
|
|
455
499
|
const min = String(args.min ?? 4)
|
|
456
|
-
const
|
|
500
|
+
const path = safePath(args.path)
|
|
501
|
+
const run = await runCmd(shell, ['strings', '-n', min, path], exec, { stdoutMaxBytes: 1048576 })
|
|
457
502
|
let lines = requireOk(run, `strings -n ${min}`).split('\n').filter(Boolean)
|
|
458
503
|
if (args.grep) {
|
|
459
504
|
const g = args.grep.toLowerCase()
|
|
460
505
|
lines = lines.filter((l) => l.toLowerCase().includes(g))
|
|
461
506
|
}
|
|
462
507
|
if (lines.length === 0) {
|
|
463
|
-
return
|
|
464
|
-
|
|
465
|
-
:
|
|
508
|
+
return JSON.stringify({
|
|
509
|
+
tool: 'bin_strings',
|
|
510
|
+
target: args.path,
|
|
511
|
+
operation: `strings -n ${min}` + (args.grep === undefined ? '' : ` filtered by ${JSON.stringify(args.grep)}`),
|
|
512
|
+
observed: { matched: 0, min: Number(min), grep: args.grep ?? null, dump: args.grep === undefined
|
|
513
|
+
? `no printable strings of length ${min} or more in ${args.path}`
|
|
514
|
+
: `no printable strings matching ${JSON.stringify(args.grep)} (min length ${min}) in ${args.path}` },
|
|
515
|
+
confidence: 'observed',
|
|
516
|
+
limitations: [
|
|
517
|
+
'一条都没匹配上 ⇒ 这是「没有这样的字符串」,不是「这个文件里没有字符串」',
|
|
518
|
+
'strings(1) 只看可打印字节序列:加密/压缩/UTF-16 里的字符串它看不到',
|
|
519
|
+
],
|
|
520
|
+
}, null, 2)
|
|
466
521
|
}
|
|
467
|
-
return
|
|
522
|
+
return JSON.stringify({
|
|
523
|
+
tool: 'bin_strings',
|
|
524
|
+
target: args.path,
|
|
525
|
+
operation: `strings -n ${min}` + (args.grep === undefined ? '' : ` filtered by ${JSON.stringify(args.grep)}`),
|
|
526
|
+
observed: { matched: lines.length, shown: Math.min(lines.length, 400), min: Number(min), grep: args.grep ?? null, dump: lines.slice(0, 400).join('\n') },
|
|
527
|
+
confidence: 'observed',
|
|
528
|
+
limitations: [
|
|
529
|
+
lines.length > 400 ? '只给前 400 条(observed.matched 是匹配总数):要更多就加 grep 缩小范围' : '这次没有截断(匹配数 ≤ 400)',
|
|
530
|
+
'strings(1) 只看可打印字节序列:加密/压缩/UTF-16 里的字符串它看不到',
|
|
531
|
+
],
|
|
532
|
+
}, null, 2)
|
|
468
533
|
},
|
|
469
534
|
}))
|
|
470
535
|
|
|
@@ -480,26 +545,48 @@ export function apply(ctx) {
|
|
|
480
545
|
render: (_args, value) => [{ type: 'text', text: value }],
|
|
481
546
|
},
|
|
482
547
|
async execute(args, exec) {
|
|
548
|
+
// 证据形状(规划 §5.1):四个分支都产出同一个信封;两条方法(GNU readelf / 仓库内置解析器)
|
|
549
|
+
// 必须写进 limitations,否则"本机没有 readelf"会被读成"这份 ELF 就是这样"。
|
|
550
|
+
const envelope = (observed, limitations) => JSON.stringify({
|
|
551
|
+
tool: 'bin_readelf',
|
|
552
|
+
target: args.path,
|
|
553
|
+
operation: `read ELF ${mode} (${flag}) and report it verbatim or via the built-in reader)`,
|
|
554
|
+
observed,
|
|
555
|
+
confidence: 'observed',
|
|
556
|
+
limitations,
|
|
557
|
+
}, null, 2)
|
|
483
558
|
const mode = args.mode === 'segments' || args.mode === 'dynamic' ? args.mode : 'headers'
|
|
484
559
|
const flag = mode === 'segments' ? '-l' : mode === 'dynamic' ? '-d' : '-h'
|
|
485
560
|
// GNU binutils is not part of macOS. When readelf is absent (or refuses the
|
|
486
561
|
// file) the ELF is parsed here instead: the previous revision threw
|
|
487
562
|
// "command not found" on every Mac, which made four of the five binary
|
|
488
563
|
// tools useless on the platform this app ships for.
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
564
|
+
const path = safePath(args.path)
|
|
565
|
+
const real = await runCmd(shell, ['readelf', flag, path], exec)
|
|
566
|
+
if (real.exitCode === 0 && real.stdout.trim() !== '') {
|
|
567
|
+
return envelope({ method: 'readelf', dump: real.stdout }, [
|
|
568
|
+
'GNU readelf 的输出原样返回,本工具不改写它',
|
|
569
|
+
])
|
|
570
|
+
}
|
|
571
|
+
const elf = await elfProbe(rawRun(shell, exec), path)
|
|
492
572
|
if (elf === null) {
|
|
493
|
-
const kind = requireOk(await runCmd(shell, ['file', '-b',
|
|
494
|
-
return [
|
|
573
|
+
const kind = requireOk(await runCmd(shell, ['file', '-b', path], exec), 'file -b').trim()
|
|
574
|
+
return envelope({ method: null, dump: [
|
|
495
575
|
`readelf ${flag} is unavailable on this host (${real.exitCode === 127 ? 'not installed' : `exit ${real.exitCode}`})`,
|
|
496
576
|
`${args.path} is not ELF, so a native ELF reader has nothing to report either — file(1) says: ${kind}`,
|
|
497
577
|
'for Mach-O use rev_disasm / rev_imports (objdump, otool and nm ship with macOS)',
|
|
498
|
-
].join('\n')
|
|
578
|
+
].join('\n') }, [
|
|
579
|
+
'本机没有可用的 readelf,而这个文件也不是 ELF ⇒ 两条路都给不出读数(observed.dump 里是原因)',
|
|
580
|
+
])
|
|
499
581
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
582
|
+
// 本机没有 GNU readelf ⇒ 下面三支都是**仓库内置解析器**算的,limitations 要说清。
|
|
583
|
+
const native = [
|
|
584
|
+
'本机没有 GNU readelf(macOS 不带 binutils)⇒ 这份读数是仓库内置原生解析器算的,不是 readelf 的输出',
|
|
585
|
+
'内置解析器只覆盖本工具声明的字段;readelf 的其它开关(--notes/--relocs 等)它不做',
|
|
586
|
+
]
|
|
587
|
+
if (mode === 'segments') return envelope({ method: 'native', dump: renderElfSegments(elf) }, native)
|
|
588
|
+
if (mode === 'dynamic') return envelope({ method: 'native', dump: await renderElfDynamic(elf) }, native)
|
|
589
|
+
return envelope({ method: 'native', dump: renderElfHeader(elf) }, native)
|
|
503
590
|
},
|
|
504
591
|
}))
|
|
505
592
|
|
|
@@ -517,11 +604,12 @@ export function apply(ctx) {
|
|
|
517
604
|
// 证据形状(规划 §5.1 的第一个试点):返回值是**信封 JSON**,不再是裸表格文本。
|
|
518
605
|
// 真值判据不变(observed.dump 里是 objdump 原文),新增 confidence 与 limitations ——
|
|
519
606
|
// 逼这个工具回答"这个结论看不见什么"(本仓最贵的那条教训)。
|
|
520
|
-
const
|
|
607
|
+
const path = safePath(args.path)
|
|
608
|
+
const run = await runCmd(shell, ['objdump', '-h', path], exec)
|
|
521
609
|
const ok = run.exitCode === 0 && run.stdout.trim() !== ''
|
|
522
610
|
// GNU binutils is not part of macOS. Instead of an opaque "command not found",
|
|
523
611
|
// say which method failed and what to use instead (same shape as bin_readelf).
|
|
524
|
-
const kind = ok ? null : await runCmd(shell, ['file', '-b',
|
|
612
|
+
const kind = ok ? null : await runCmd(shell, ['file', '-b', path], exec)
|
|
525
613
|
return JSON.stringify({
|
|
526
614
|
tool: 'bin_sections',
|
|
527
615
|
target: args.path,
|
|
@@ -146,6 +146,9 @@ tail -f .fly-blackbox/capture/all.log
|
|
|
146
146
|
| `bx_ca` | CA 信任状态 + **要人跑的命令文本** + `security verify-cert` 事后证据 | 不受门控(永不提权) |
|
|
147
147
|
| `bx_flows` | 离线读 dump:`hosts` / `list` / `show` / `grep`(版本闸 + 拒绝截断) | 不受门控(只读) |
|
|
148
148
|
| `bx_diff` | 改前/改后字段级 diff,显式区分**缺失** / **假值** / **类型变化** | 不受门控(只读) |
|
|
149
|
+
| `bx_cdp_targets` | **进程内通道第一件**:列 `127.0.0.1:<port>` 上可附加的 target(page/worker/iframe)。只走回环、端口只认参数、控制通道地址不打印;整件在 `FLY_ALLOW_INTERCEPT` 门后 |
|
|
150
|
+
| `bx_cdp_scripts` | 附加上去收 `Debugger.scriptParsed`:**已解析**脚本清单(会回放已解析的;**不返回源码**;窗口一结束就断开) |
|
|
151
|
+
| `bx_cdp_coverage` | 精确覆盖率:采一段窗口 → `takePreciseCoverage` → 逐脚本"已执行字节"(区间**求并集**;拿不到快照就**大声失败**,不给半份) |
|
|
149
152
|
|
|
150
153
|
规则文件固定 `<workspace>/.fly-blackbox/rules.json`,由**你自己用文件工具写**,`bx_proxy start` 只读并校验;
|
|
151
154
|
schema 与"显式 off、无静默默认值"的纪律见 `reference/rules.schema.md`。
|