fix(sandbox): evidence-gate runner failures (round 1)

This commit is contained in:
Hypatia May
2026-08-03 17:31:08 +08:00
parent 6a3caea277
commit 6343d8f6e6
51 changed files with 805 additions and 140 deletions

View File

@@ -235,7 +235,7 @@ export type Config = LocalConfig
Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local)
Source: [`packages/bash/bash-sandbox/src/index.ts:27`](../packages/bash/bash-sandbox/src/index.ts)
Source: [`packages/bash/bash-sandbox/src/index.ts:34`](../packages/bash/bash-sandbox/src/index.ts)
## `@deepseek-ai/dsh-cli-demo`
@@ -1037,7 +1037,7 @@ export interface Config {
}
```
Source: [`packages/sandbox/sandbox-local/src/index.ts:19`](../packages/sandbox/sandbox-local/src/index.ts)
Source: [`packages/sandbox/sandbox-local/src/index.ts:24`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`

View File

@@ -1095,7 +1095,7 @@ abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
Types: [ConfinedArgv](../core-data-structures/sandbox.md) · [SandboxPolicy](../core-data-structures/sandbox.md)
Source: [`packages/sandbox/sandbox/src/index.ts:131`](../../packages/sandbox/sandbox/src/index.ts)
Source: [`packages/sandbox/sandbox/src/index.ts:148`](../../packages/sandbox/sandbox/src/index.ts)
## `ctx.sandboxPolicy` — `SandboxPolicyService`

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
sandbox.md: 9bc05fa06f22fdc9ac9e8aacd482c1e7c2f2edec
sandbox.zh.md: 9a52f126758fe0e7988715c7824e963bd6e6ea84
# pnpm run verify-translation-pairing --write docs/core-data-structures/sandbox.md
sandbox.md: 8483104e0aab78779acb52b232a205280c93cd2b
sandbox.zh.md: b283095f1c847a4b2fca5a109e7894d6e3479839

View File

@@ -87,7 +87,27 @@ interface SandboxPolicy extends SandboxExecutionPolicy {
## Wrapped argv and classification dialects
`ConfinedArgv` is what the consumer spawns. Besides the replacement argv, it carries the backend's enforcement fact and two orthogonal stderr dialects. `denialSignatures` identify the confined command being blocked while the sandbox works correctly. `runnerFailureSignatures` identify the sandbox runner refusing or failing before it executes the command; consumers check these first and surface a sandbox infrastructure failure, never an ordinary task failure.
`RunnerFailureRule` combines evidence that a runner failed before executing the command. A consumer requires a nonzero exit, the optional allowed-exit-code gate, and a case-insensitive fatal signature within one remaining stderr line. Case-insensitive exact full-line informational exclusions are removed first, so a benign runner notice cannot prove failure by itself. The matched line remains available as error detail; classification does not rewrite stderr.
```ts type-equiv
/**
* Evidence that identifies a sandbox runner failing before it executes the
* wrapped command. A consumer first applies {@link allowedExitCodes} when
* present, removes {@link informationalLines} by case-insensitive exact line
* equality, then matches {@link fatalSignatures} case-insensitively within
* each remaining stderr line. Exit status alone never proves runner failure.
*/
interface RunnerFailureRule {
/** Nonzero process exit codes on which this rule may match; omitted permits any nonzero exit. */
allowedExitCodes?: readonly number[]
/** Non-empty substrings identifying a fatal runner diagnostic on one stderr line. */
fatalSignatures: readonly string[]
/** Benign stderr lines excluded by exact full-line equality before fatal matching. */
informationalLines?: readonly string[]
}
```
`ConfinedArgv` is what the consumer spawns. Besides the replacement argv, it carries the backend's enforcement fact and two orthogonal stderr classifiers. `denialSignatures` identify the confined command being blocked while the sandbox works correctly. `runnerFailureRules` identify the sandbox runner refusing or failing before it executes the command; consumers check these first and surface a sandbox infrastructure failure, never an ordinary task failure.
```ts type-equiv
/**
@@ -110,18 +130,19 @@ interface ConfinedArgv {
*/
denialSignatures: readonly string[]
/**
* Case-insensitive signatures for runner failure before command execution.
* Consumers check these before denial signatures: runner failure means the
* Structured runner-failure evidence rules. Consumers require a matching
* fatal stderr line (after informational exclusions) and any rule-specific
* exit-code gate before checking denial signatures: runner failure means the
* command never ran, while denial means confinement worked and blocked it.
*/
runnerFailureSignatures: readonly string[]
runnerFailureRules: readonly RunnerFailureRule[]
}
```
An operator-configured local runner must supply at least one `runnerFailureSignatures` entry for its own pre-exec refusal dialect; the provider adds outer-shell missing and unexecutable forms automatically. This makes an executable custom runner rejecting its profile distinguishable from the wrapped command exiting with the same status.
The operator-facing local-provider key remains `runnerFailureSignatures`: an operator-configured runner must supply at least one entry for its own pre-exec refusal dialect. The provider maps those entries into one rule and adds a separate argv0-scoped rule for outer-shell missing or unexecutable failures. This makes an executable custom runner rejecting its profile distinguishable from the wrapped command exiting with the same status.
## Provider and fail-closed errors
`ctx.sandbox.confine(argv, policy)` returns a `ConfinedArgv` or throws `SandboxUnavailableError` with code `SANDBOX_UNAVAILABLE` when no usable backend exists. A selected runner can also fail closed at execution time, in which case its failure signature carries the same infrastructure meaning. Silent unconfined passthrough is never legal for a confined policy.
`ctx.sandbox.confine(argv, policy)` returns a `ConfinedArgv` or throws `SandboxUnavailableError` with code `SANDBOX_UNAVAILABLE` when no usable backend exists. A selected runner can also fail closed at execution time, in which case a matching structured rule carries the same infrastructure meaning. Silent unconfined passthrough is never legal for a confined policy.
Provider probing arbitrates between multiple candidates and is cached for the provider lifetime. A platform with one candidate may select it directly; execution-time refusal retains the safety property. The local provider reports bwrap and Seatbelt as full and preserves the Landlock launcher's full/partial kernel verdict.

View File

@@ -87,7 +87,27 @@ interface SandboxPolicy extends SandboxExecutionPolicy {
## 包装后的 argv 与分类方言
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv它还携带后端的强制执行事实和两种正交的 stderr 方言。`denialSignatures` 用于识别沙箱正常工作时被隔离命令被阻止的情况。`runnerFailureSignatures` 用于识别沙箱运行器在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障上报,而非普通任务失败
`RunnerFailureRule` 汇集用于判定 runner 在执行命令前失败的证据。消费方要求进程以非零状态退出,并同时满足可选的允许退出码门控,以及余下某一 stderr 行中不区分大小写的致命签名。系统会先按不区分大小写的整行精确匹配移除信息性排除项,因此无害的 runner 通知本身不能证明失败。匹配到的行仍可用作错误详情;分类过程不会重写 stderr
```ts type-equiv
/**
* Evidence that identifies a sandbox runner failing before it executes the
* wrapped command. A consumer first applies {@link allowedExitCodes} when
* present, removes {@link informationalLines} by case-insensitive exact line
* equality, then matches {@link fatalSignatures} case-insensitively within
* each remaining stderr line. Exit status alone never proves runner failure.
*/
interface RunnerFailureRule {
/** Nonzero process exit codes on which this rule may match; omitted permits any nonzero exit. */
allowedExitCodes?: readonly number[]
/** Non-empty substrings identifying a fatal runner diagnostic on one stderr line. */
fatalSignatures: readonly string[]
/** Benign stderr lines excluded by exact full-line equality before fatal matching. */
informationalLines?: readonly string[]
}
```
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv它还携带后端的强制执行事实和两种正交的 stderr 分类器。`denialSignatures` 用于识别沙箱正常工作时被隔离命令被阻止的情况。`runnerFailureRules` 用于识别沙箱 runner 在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障上报,而非普通任务失败。
```ts type-equiv
/**
@@ -110,18 +130,19 @@ interface ConfinedArgv {
*/
denialSignatures: readonly string[]
/**
* Case-insensitive signatures for runner failure before command execution.
* Consumers check these before denial signatures: runner failure means the
* Structured runner-failure evidence rules. Consumers require a matching
* fatal stderr line (after informational exclusions) and any rule-specific
* exit-code gate before checking denial signatures: runner failure means the
* command never ran, while denial means confinement worked and blocked it.
*/
runnerFailureSignatures: readonly string[]
runnerFailureRules: readonly RunnerFailureRule[]
}
```
运维人员配置的本地运行器必须为自身的 pre-exec 拒绝方言提供至少一条 `runnerFailureSignatures` 条目提供方会自动添加外层 shell 的 missing 和 unexecutable 形式。这使得可执行的自定义运行器拒绝其 profile 的情况能够与被包装命令以相同状态码退出的情况区分开来。
面向运维人员的本地提供方配置键仍为 `runnerFailureSignatures`:运维人员配置的 runner 必须为自身的 pre-exec 拒绝方言提供至少一条条目提供方会将这些条目映射到一条规则,并另加一条按 argv0 限定的外层 shell 规则,用于识别命令缺失或不可执行。这使得可执行的自定义 runner 拒绝其 profile 的情况能够与被包装命令以相同状态码退出的情况区分开来。
## 提供方与 fail-closed 错误
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。已选定的运行器也可能在执行时 fail-closed此时其失败签名承载相同的基础设施含义。对于受限策略,静默的无隔离透传永远不合法。
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。已选定的 runner 也可能在执行时 fail-closed此时匹配到的结构化规则承载相同的基础设施含义。对于受限策略,静默的无隔离透传永远不合法。
提供方探测在多个候选后端之间仲裁,结果在提供方生命周期内缓存。只有一个候选后端的平台可以直接选定它;执行时拒绝仍保留安全属性。本地提供方将 bwrap 和 Seatbelt 报告为 full并保留 Landlock 启动器的 full/partial 内核裁定。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md
0004-landlock-partial-notice-misclassified-child-failures.md: ccedba6094ffec9de95fbd52d58add4a0e6e5105
0004-landlock-partial-notice-misclassified-child-failures.zh.md: 89d85d835d58753721927be85a5581b12a490fcf

View File

@@ -0,0 +1,55 @@
# Post-mortem 0004: Landlock partial-enforcement notice misclassified child failures
English | [中文](0004-landlock-partial-notice-misclassified-child-failures.zh.md)
Status: resolved
## Executive summary
On kernels with an older Landlock ABI, the launcher prints a benign partial-enforcement notice before executing every child. The harness treated that shared `landlock-run:` prefix plus any nonzero child exit as launcher failure, so ordinary outcomes such as ripgrep's exit 1 for no matches surfaced as `SANDBOX_UNAVAILABLE`; filesystem search then hid that structured error behind `SEARCH_FAILED`. Broad signature rules and missing partial-ABI composition coverage let the defect through. Runner classification now requires status-gated fatal evidence after exact informational exclusions, search preserves structured executor errors, and an assembled keyless scenario pins the affected product path.
## Summary
The native launcher contract distinguishes two kinds of stderr lines. A partially enforcing kernel prints exactly `landlock-run: partial enforcement (older Landlock ABI)` and continues into the child. A launcher failure prints another `landlock-run:` line and exits 125 without executing the child.
The harness represented both with one case-insensitive `landlock-run: ` substring. Its consumer classified any nonzero exit carrying that substring as runner failure. The child's status was therefore attached to the launcher's informational line: `false`, ripgrep's no-match exit 1, invalid-pattern exit 2, and even a child-selected exit 125 could be blamed on the sandbox despite successful confinement and execution.
Filesystem search added a second attribution error. `runRipgrep()` caught every rejected bash run that was not aborted and replaced it with a generic cwd/shell-start `SEARCH_FAILED`, including the structured `SandboxUnavailableError` produced by the sandbox executor.
## Impact
On partial-ABI Landlock hosts, legitimate nonzero child outcomes could appear as sandbox infrastructure failure. `glob` and `grep` were especially visible because ripgrep uses exit 1 as successful empty search. When a real sandbox failure did occur through filesystem search, callers lost its `SANDBOX_UNAVAILABLE` code and received an incorrect startup diagnosis.
The defect did not weaken confinement or run a command unconfined. Its security effect was availability and diagnostic integrity: a valid confined result was rejected or mislabeled.
## Timeline
- The native launcher contract defined exit 125 for launcher failures, a fatal `landlock-run:` line for every such failure, and the exact partial-enforcement notice for successful child execution.
- The sandbox provider reduced that contract to `runnerFailureSignatures: ['landlock-run: ']`; the bash consumer combined the prefix with any nonzero exit and reported stderr's first line.
- Unit tests covered clean success, denial diagnostics, and fatal runner prefixes. Real-runner tests self-skipped without a usable kernel and did not force partial enforcement followed by a nonzero child.
- A minimal POSIX wrapper that prints the notice and `exec`s its payload reproduced the failure with `false` and ripgrep no-match.
- Structured rules, shared foreground/background classification, search error preservation, and assembled replay coverage closed the two attribution gaps.
## Root cause
The public sandbox result type could express only a bag of substrings. It could not state that Landlock failure requires exit 125, that evidence must occur within one fatal line, or that one exact line under the same prefix is informational. The boolean consumer consequently joined unrelated facts from different processes and selected the first stderr line for detail even when a later line was the fatal evidence.
The test matrix mirrored that representation. Fake providers emitted either no runner line or an unambiguously fatal prefix; they never emitted a benign runner line before a child-controlled nonzero exit. Real Landlock coverage depended on the host ABI, so full-ABI hosts could not exercise the notice. Filesystem-search tests modeled raw spawn errors but not a structured error thrown by the real sandboxed bash composition.
Stderr remains an in-band attribution channel. A confined child can deliberately reproduce a runner's gated fatal line and exit status, causing an availability/diagnostic false attribution. The tighter conjunction prevents the accidental collision in this incident but does not authenticate the writer; an out-of-band status protocol remains separate hardening, not a sandbox-bypass fix.
## Guardrails added
- [`RunnerFailureRule`](../core-data-structures/sandbox.md#wrapped-argv-and-classification-dialects) carries optional allowed exit codes, case-insensitive per-line fatal signatures, and case-insensitive exact informational-line exclusions.
- [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) maps Landlock to exit 125 plus a non-notice `landlock-run:` line, keeps bwrap/Seatbelt/custom behavior, and separates argv0-scoped outer-shell failures using exit 126/127.
- [`dsh-bash-sandbox`](../../packages/bash/bash-sandbox/) uses one evidence-returning classifier for foreground and background execution. Fatal evidence outranks denial, and foreground errors report the matched fatal line without changing captured stderr.
- [`dsh-tool-fs-search`](../../packages/fs/tool-fs-search/) retains abort as `SEARCH_ABORTED`, propagates existing `HarnessError` instances unchanged, and applies `SEARCH_FAILED` only to untyped start failures. Ripgrep still owns exit 0/1/other semantics inside the adapter.
- Deterministic tests use a POSIX fake partial-Landlock launcher to cover `true`, `false`, child exit 125, permission denial, real fatal diagnostics, and foreground/background parity. A real search composition covers empty grep/glob, invalid regex, and `SANDBOX_UNAVAILABLE` propagation.
- The `examples/acp-agent` keyless snapshot runs direct bash `false` through a test-only partial-Landlock provider, keeping the product regression pinned independently of filesystem-search implementation choices.
## Lessons
- Process attribution requires a conjunction of independent evidence; a shared prefix is not a protocol.
- Informational and fatal diagnostics can share a namespace, so exclusions must be exact and narrow while unknown fatal lines stay fail-closed.
- An adapter must preserve structured failures owned by the seam below it instead of replacing them with its own nearest generic category.
- Platform-dependent behavior needs a deterministic fake at the native boundary plus one assembled product path; a self-skipping real-kernel test cannot carry that regression alone.

View File

@@ -0,0 +1,55 @@
# 事故复盘postmortem 0004Landlock 部分强制执行通知导致子进程失败被误归类
[English](0004-landlock-partial-notice-misclassified-child-failures.md) | 中文
Status: resolved
## 摘要
在 Landlock ABI 较旧的内核上launcher 会在执行每个子进程前打印一条无害的部分强制执行通知。harness 把共享的 `landlock-run:` 前缀与任意非零子进程退出组合起来,判定为 launcher 失败,因此 ripgrep 在没有匹配项时以 1 退出等普通结果会呈现为 `SANDBOX_UNAVAILABLE`;文件系统搜索随后又用 `SEARCH_FAILED` 遮蔽了这个结构化错误。过于宽泛的签名规则,以及缺少较旧 ABI 下部分强制执行的组合测试覆盖让该缺陷得以流入。runner 分类现在会先精确排除信息性行,再要求由退出状态门控的致命证据;搜索会保留结构化执行器错误,并由一个组装后的无密钥场景固定受影响的产品路径。
## 概述
原生 launcher 契约区分两类 stderr 行。内核只能部分强制执行时,会精确打印 `landlock-run: partial enforcement (older Landlock ABI)`然后继续执行子进程。launcher 失败则打印另一行 `landlock-run:` 诊断,在不执行子进程的情况下以 125 退出。
harness 用一个不区分大小写的 `landlock-run: ` 子串表示这两种情况。消费方只要发现非零退出同时携带该子串,就会归类为 runner 失败。因此,子进程的退出状态被错误地关联到 launcher 的信息性行:`false`、ripgrep 无匹配时的退出码 1、无效 pattern 的退出码 2乃至由子进程自行选择的退出码 125都可能在约束与执行均成功的情况下被错误归因为沙箱故障。
文件系统搜索又造成第二处归因错误。`runRipgrep()` 会捕获 bash 执行器除中止外抛出的所有错误,并将其替换为关于 cwd 或 shell 启动的通用 `SEARCH_FAILED`,其中也包括沙箱执行器产生的结构化 `SandboxUnavailableError`
## 影响
在 Landlock ABI 只能部分强制执行的主机上,合法的非零子进程结果可能表现为沙箱基础设施故障。`glob``grep` 尤其容易暴露该问题,因为 ripgrep 把退出码 1 用作成功的空搜索。当文件系统搜索中确实发生沙箱故障时,调用方也会丢失其 `SANDBOX_UNAVAILABLE` 错误码,转而收到错误的启动诊断。
该缺陷没有削弱约束,也没有让命令在无约束状态下运行。其安全影响在于可用性与诊断完整性:有效的受限结果会被拒绝或错误标记。
## 时间线
- 原生 launcher 契约规定launcher 失败使用退出码 125每次此类失败都会打印一行致命的 `landlock-run:` 诊断;成功执行子进程时则打印精确的部分强制执行通知。
- 沙箱提供方把该契约简化为 `runnerFailureSignatures: ['landlock-run: ']`bash 消费方将此前缀与任意非零退出组合,并报告 stderr 的第一行。
- 单元测试覆盖了无诊断的成功、拒绝诊断和致命 runner 前缀。真实 runner 测试在没有可用内核时会自行跳过,也没有强制构造「部分强制执行通知后跟非零子进程退出」的情况。
- 一个最小 POSIX 包装脚本会打印该通知并 `exec` 其负载;它通过 `false` 与 ripgrep 无匹配场景复现了故障。
- 结构化规则、前台与后台共享的分类逻辑、搜索错误保留和组装后的回放覆盖共同弥补了这两处归因缺口。
## 根因
公开的沙箱结果类型只能表达一组子字符串。它无法表示 Landlock 失败必须使用退出码 125、证据必须出现在一行致命诊断内或同一前缀下有一行精确文本属于信息性通知。消费方的布尔判定逻辑因此把来自不同进程且互不相关的事实组合在一起即便致命证据位于后续行它仍选用 stderr 的第一行作为详细信息。
测试矩阵与这种表示方式一致。模拟提供方要么不输出 runner 行,要么输出含义明确的致命前缀,从不在由子进程控制的非零退出前输出无害 runner 行。真实 Landlock 覆盖依赖主机 ABI因此使用完整 ABI 的主机无法覆盖该通知。文件系统搜索测试模拟了原始 spawn 错误,却没有覆盖真实沙箱化 bash 组合抛出的结构化错误。
stderr 仍是带内归因通道。受限子进程可以故意复现 runner 的门控致命诊断行与退出状态,造成可用性或诊断误归因。更严格的多项证据合取可以避免本次事故中的意外冲突,但无法验证写入者身份;带外状态协议仍属于独立的加固工作,而非沙箱绕过修复。
## 已添加的防护措施
- [`RunnerFailureRule`](../core-data-structures/sandbox.md#wrapped-argv-and-classification-dialects) 携带可选的允许退出码、不区分大小写的逐行致命签名,以及按不区分大小写的整行精确匹配排除的信息性行。
- [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) 把 Landlock 映射为退出码 125 加一行非通知的 `landlock-run:` 诊断,保留 bwrapSeatbelt自定义 runner 的行为,并以退出码 126127 单独处理按 argv0 限定的外层 shell 失败。
- [`dsh-bash-sandbox`](../../packages/bash/bash-sandbox/) 为前台与后台执行使用同一个返回证据的分类器。致命证据优先于拒绝,前台错误会报告匹配到的致命行,同时保持捕获的 stderr 不变。
- [`dsh-tool-fs-search`](../../packages/fs/tool-fs-search/) 把中止保留为 `SEARCH_ABORTED`,原样传播既有 `HarnessError` 实例,并且仅将无类型的启动失败归为 `SEARCH_FAILED`。适配器内部仍按 ripgrep 自身的退出码 01其他语义处理。
- 确定性测试使用一个模拟 Landlock 部分强制执行行为的 POSIX launcher覆盖 `true``false`、子进程退出码 125、权限拒绝、真实致命诊断以及前台后台一致性。真实搜索组合覆盖空 grepglob、无效正则表达式和 `SANDBOX_UNAVAILABLE` 传播。
- `examples/acp-agent` 的无密钥快照会通过仅用于测试的部分 Landlock 提供方直接运行 bash `false`,从而独立于文件系统搜索的实现选择固定产品层回归。
## 教训
- 进程归因需要多项独立证据同时成立;共享前缀不是协议。
- 信息性诊断与致命诊断可以共享同一命名空间,因此排除规则必须精确且范围狭窄,同时对未知的致命行保持失败关闭。
- 适配器必须保留下层 seam 所拥有的结构化失败,而不能用自身最接近的通用类别将其替换。
- 平台相关行为需要在原生边界放置确定性的模拟实现,并覆盖一条组装后的产品路径;会自行跳过的真实内核测试无法独自固定该回归。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/postmortem/README.md
README.md: 4858f8841e92a895f2d1a840b59b42758e83d952
README.zh.md: 127eb19422f1eb6738c1d246791064096f4382f9
README.md: ffde0057304856b7c7718e3dd1f4743c48ee193f
README.zh.md: cf8cfef52bf006619cf31087756fc8b3555b707d

View File

@@ -15,3 +15,4 @@ Every post-mortem opens with an **Executive summary**: one short paragraph a bus
| [0001](0001-acp-default-export-drops-inject.md) | ACP server crashed on connect: `export default` dropped the plugin's `inject` |
| [0002](0002-js-expression-disabled-filesystem-tools.md) | Filesystem snapshot tools were permanently disabled by a literal `!!js` object |
| [0003](0003-web-agent-gui-feedback-loop.md) | Web agent validated a replacement server instead of the GUI hosting its session |
| [0004](0004-landlock-partial-notice-misclassified-child-failures.md) | Landlock partial-enforcement notice misclassified child failures |

View File

@@ -15,3 +15,4 @@
| [0001](0001-acp-default-export-drops-inject.md) | ACPAgent Client Protocol服务器在连接时崩溃`export default` 丢失了插件的 `inject` |
| [0002](0002-js-expression-disabled-filesystem-tools.md) | 文件系统快照工具被一个字面量 `!!js` 对象永久禁用 |
| [0003](0003-web-agent-gui-feedback-loop.md) | Web agent 验证了替代服务器,而非承载其会话的 GUI |
| [0004](0004-landlock-partial-notice-misclassified-child-failures.md) | Landlock 部分强制执行通知导致子进程失败被误归类 |