| doc-id | 09-local-protocol-security | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| title | 本地协议与安全 | |||||||||||
| status | active | |||||||||||
| version | 1.9.0 | |||||||||||
| last-updated | 2026-09-05 | |||||||||||
| source-range | 规格书 §2.3、§9.1 末段、§12.3–§12.4、§14–§15、§25.2;v1.1–v1.7.1 既有协议;v1.8 组件数据链路只读 endpoint 与最小化 observation | |||||||||||
| 参考文献/依赖 |
|
Astro 补充(2026-09-05):源码扩展名白名单新增 .astro,但仍须会话 registry、realpath root、大小预算与 token/Origin 校验。astro-host 目标使用现有 annotation authorizer 重新读取、授权和裁剪源码,不信任浏览器提交的片段。中间件同 origin 挂载,不另起端口;Astro base 不改变 /__spotpatch 协议根路径。build/preview/sync 不挂载中间件。详见 Astro 安全设计。
本文件是 HTTP 路径、请求头、错误码、浏览器数据边界、清洗规则、文件授权、会话安全和安全验收的唯一事实来源。fileId 的生成和 registry 行为由 Vite 插件规范定义 (见 doc-id:04-vite-plugin),配置值域和默认值由公共 API 文档定义 (见 doc-id:03-public-api-models)。
浏览器不持有绝对路径、provider 凭据或本地执行能力。
- DOM 属性只保存会话内的短 source ID、行号和列号。
- 文件绝对路径只存在于 Vite Node 端内存。
- 源码仅在用户完成选择后按需读取。
- 默认只返回有限代码片段,不返回完整文件。
- AI UI 只持有 provider/model profile ID、显示名、脱敏能力状态和公共 Job 数据。
- 数据链路 UI 只持有严格 report 和当前页面内存 observation;不得持有 raw URL、body、header、cookie、Response、Fiber、函数、绝对路径或原始 stack。
- 浏览器不得接收 Base URL、真实 provider 模型名、Key 环境变量名、Authorization header、worktree 真实路径、命令 cwd 或完整进程环境。
禁止把项目 root、绝对路径、编辑器命令、Agent check 命令和 Secret Resolver 注入浏览器。页面脚本、DOM 内容或浏览器请求参数都不能扩大服务端配置的文件、模型和执行权限。
源码片段的业务边界和预算见源码片段与 Prompt 规范 (见 doc-id:08-code-prompt)。
value、defaultValuepasswordauthorizationcookie、set-cookie- token、secret、api-key 常见变体
- 内联 base64
- blob URL
- 超长 SVG path
- React 私有 expando 和事件对象
密码输入框无论用户是否关闭 redact 都必须脱敏。
DOM 预览必须通过 textContent 写入 <pre>。禁止将采集到的 HTML 重新写入 innerHTML,否则业务页面内容可能在工具 UI 中形成 XSS。
采集范围由 DOM/CSS 采集规范定义,采集器只消费本节清洗规则,不得维护另一份名单 (见 doc-id:07-dom-css-collection)。
基础路径、请求头和 endpoint 只在此处定义:
export const SPOTPATCH_API_BASE = "/__spotpatch/v1" as const;
export const SPOTPATCH_TOKEN_HEADER = "X-SpotPatch-Token" as const;
export const SPOTPATCH_ENDPOINTS = {
sourceContext: `${SPOTPATCH_API_BASE}/source-context`,
openEditor: `${SPOTPATCH_API_BASE}/open-editor`,
dataFlowComponentReport: `${SPOTPATCH_API_BASE}/data-flow/component-report`,
dataFlowPageReport: `${SPOTPATCH_API_BASE}/data-flow/page-report`,
agentCapability: `${SPOTPATCH_API_BASE}/agent/capability`,
agentWorkspaceHealth: `${SPOTPATCH_API_BASE}/agent/workspace-health`,
agentJobs: `${SPOTPATCH_API_BASE}/agent/jobs`,
} as const;
export type AgentJobAction =
| "events"
| "result"
| "cancel"
| "apply"
| "revert";
export function getAgentJobEndpoint(
jobId: string,
action: AgentJobAction,
): string {
return `${SPOTPATCH_ENDPOINTS.agentJobs}/${encodeURIComponent(jobId)}/${action}`;
}客户端与服务端必须从 shared/protocol 引用这些常量,不得复制字符串。
- endpoint 请求统一使用带 JSON body 的
POST;Job 事件 endpoint 的响应为application/x-ndjson。 - 使用
X-SpotPatch-Token请求头。 - 普通请求体最大 32 KB;只有创建 Agent Job 的请求因包含有界多目标 DOM/CSS 上下文而允许最大 256 KB。该例外不适用于 capability、事件、结果、编辑器或源码接口,也不改变字段级 Schema 上限。
- 响应设置
Cache-Control: no-store。 - 校验 Origin;默认只允许当前 Vite origin 和 loopback。
- 事件和结果读取也必须发送空 JSON 对象。不能改为无 body 的浏览器
GET,因为同源 GET 不保证携带Origin,会破坏强制 Origin 校验。 - 所有返回都使用统一 envelope。
- 写操作禁止浏览器自动重试;服务端使用 Job ID 和 action 保证本地协议副作用幂等。provider 工具副作用使用
turn + toolCallId作为执行作用域,不能把 provider 原始 ID 当成整个 Job 的全局唯一键。
type ApiSuccess<T> = {
ok: true;
data: T;
};
type ApiFailure = {
ok: false;
error: {
code: ErrorCode;
message: string;
};
};错误信息不能返回绝对路径、调用栈和内部异常对象;debug 信息只写本地终端。
NDJSON 流是统一 envelope 的唯一例外。事件联合只在此处定义:
interface AgentJobEventBase {
readonly schemaVersion: 2;
readonly sequence: number;
readonly jobId: string;
readonly status: AgentJobStatus;
readonly timestamp: string;
}
export type AgentJobEvent =
| (AgentJobEventBase & {
readonly type: "snapshot";
readonly data: { readonly snapshot: AgentJobSnapshot };
})
| (AgentJobEventBase & {
readonly type: "phase";
readonly data: { readonly message: string };
})
| (AgentJobEventBase & {
readonly type: "tool";
readonly data: {
readonly turn: number;
readonly toolCallId: string;
readonly toolName: string;
readonly state: "started" | "succeeded" | "failed";
readonly relativePath?: string;
readonly checkLabel?: string;
};
})
| (AgentJobEventBase & {
readonly type: "check";
readonly data: { readonly result: AgentCheckResult };
})
| (AgentJobEventBase & {
readonly type: "result-ready";
readonly data: { readonly hasResult: true };
})
| (AgentJobEventBase & {
readonly type: "error";
readonly data: {
readonly code: ErrorCode;
readonly message: string;
};
});每行必须通过联合 Schema,并使用 Job 内严格递增、不可复用的 sequence。turn 是 SpotPatch 从 1 开始生成的模型轮次,不由浏览器或 provider 提供;Runtime 使用 turn + toolCallId 关联同一次工具活动,避免中转站跨轮复用原始 ID 时覆盖历史状态。同一轮内相同 ID 的冲突调用必须拒绝。事件不包含原始 provider chunk、Prompt、完整文件、patch、命令、环境或异常;完整 Diff 只通过认证后的 result endpoint 获取。客户端只处理完整换行记录;流中断后通过 Job 快照和 result endpoint 收敛状态,不猜测最后操作是否成功。
方法与 endpoint:POST SPOTPATCH_ENDPOINTS.sourceContext。
请求示例:
{
"fileId": "Q7k3pA9vL2s",
"line": 36,
"column": 5,
"maxLines": 80
}响应示例:
{
"ok": true,
"data": {
"relativePath": "src/components/UserProfile.tsx",
"language": "tsx",
"startLine": 12,
"endLine": 58,
"excerpt": "...",
"boundary": "component"
}
}示例中的 maxLines 使用当前公共默认预算;规范值只由 ContextBudget.maxCodeLines 定义 (见 doc-id:03-public-api-models)。
方法与 endpoint:POST SPOTPATCH_ENDPOINTS.openEditor。
请求只接受 fileId、line、column。不能接受:
- 绝对路径
- 任意 editor 名称
- shell command
- CLI arguments
编辑器偏好来自已解析的服务端配置,只接受公共枚举 (见 doc-id:03-public-api-models)。auto 先从 Vite 进程的受控集成环境识别 Cursor/VS Code,无法识别时才进入后备探测;显式 vscode/cursor 只映射到固定命令。参数、工作区路由和探测优先级只由 Vite 插件规范定义 (见 doc-id:04-vite-plugin),浏览器无权选择或扩大该配置。
服务端必须等待 editor adapter 的短暂启动确认;在确认窗口内出现命令不存在、非零退出或启动失败时返回统一脱敏错误,不能提前返回 200。成功响应为 { editor: "auto" | "vscode" | "cursor" },其中 editor 是适配器实际采用的受控偏好,不得机械回显配置值。该响应表示请求已通过即时启动检查,不承诺操作系统一定把窗口置前。Runtime 必须校验响应枚举,不接受服务端或页面注入的任意命令字符串。
Apply 后可以保留会话内 fileId 与行列用于只读导航,但必须释放已失效的 DOM 引用;文件仍需在每次请求时重新通过 registry 和 root allowlist 授权。
以下 endpoint 只在服务端可信配置显式启用 dataFlow 时可用;关闭时返回 DATA_FLOW_DISABLED,不初始化 analyzer,也不泄露依赖:
| 操作 | 方法与 endpoint | 请求摘要 | 成功语义 |
|---|---|---|---|
| 组件报告 | POST SPOTPATCH_ENDPOINTS.dataFlowComponentReport |
schemaVersion: 1 + exact componentSourceId/sourceVersion,或已授权 fileId/line/column |
返回严格、已清洗、不可变的静态组件依赖报告 |
| 页面报告 | POST SPOTPATCH_ENDPOINTS.dataFlowPageReport |
schemaVersion: 1 + 有界 targets |
对目标报告去重聚合,不接收浏览器 observation 原值 |
exact identity 必须由当前 Source Registry 解析且 sourceVersion 完全一致;缺失或过期返回 DATA_FLOW_SOURCE_STALE,不得搜索相似组件。坐标模式仍只接受 Registry fileId,并在 realpath 后验证项目根。请求不得包含 root、绝对/相对 path、glob、include、module list、adapter、预算、URL 或请求/响应值。
数据链路请求体受 DEFAULT_DATA_FLOW_LIMITS.protocolRequestMaxBytes 约束,报告受独立最大字节数和客户端结构校验约束。Runtime observation 不上传 Node;它只保存在浏览器有界 ring buffer,并通过 opaque callsite/sourceVersion 在本地合并。URL 只保留 origin、pathname、query key,SpotPatch 自身 API 流量在 recorder 入口排除。详细模型和保留周期见 (见 doc-id:data-flow-07-model-protocol-config)、(见 doc-id:data-flow-10-security-performance)。
以下 endpoint 只在可信配置显式启用 AI 时注册;Agent 未启用时统一返回 AI_DISABLED,不能泄露 provider 是否配置:
| 操作 | 方法与 endpoint | 请求摘要 | 成功语义 |
|---|---|---|---|
| 能力探测 | POST SPOTPATCH_ENDPOINTS.agentCapability |
providerProfileId、modelProfileId |
返回显示名、脱敏 capability 状态和建议动作 |
| 工作区健康 | POST SPOTPATCH_ENDPOINTS.agentWorkspaceHealth |
空 JSON 对象 | 返回 ready、consent-required 或 blocked,以及去重后的变更计数和稳定错误码 |
| 创建 Job | POST SPOTPATCH_ENDPOINTS.agentJobs |
annotation、providerProfileId、modelProfileId、applyMode、值必须为 true 的 providerDataConsent、可选且只能为 true 的 trustedFastModeConsent、workingTreeMode |
202,返回初始 AgentJobSnapshot |
| 读取事件 | POST getAgentJobEndpoint(id, "events") |
空 JSON 对象 | 返回当前会话内有序 NDJSON 事件 |
| 读取结果 | POST getAgentJobEndpoint(id, "result") |
空 JSON 对象 | 返回公共 Job 快照及可用的 AgentJobResult |
| 取消 | POST getAgentJobEndpoint(id, "cancel") |
空 JSON 对象 | 进入 cancelling 或返回已终态快照 |
| 应用 | POST getAgentJobEndpoint(id, "apply") |
空 JSON 对象 | 重新校验基线并全有或全无应用 |
| 撤销 | POST getAgentJobEndpoint(id, "revert") |
空 JSON 对象 | 哈希匹配后全有或全无撤销 |
创建 Job 时客户端不能提供 URL、Key、环境变量名、真实模型名、root、路径、命令、limits、检查集合或 system prompt。服务端必须用 profile ID 从已解析配置重建全部可信值,并重新校验 SpotAnnotation、会话、来源和模型 allowlist。applyMode 只能从服务端公开的策略内选择:服务端为 trusted-auto 时允许本次请求选择 review | trusted-auto,其他策略只接受与服务端完全一致的模式;客户端不能借此扩大权限。workingTreeMode 只接受 require-clean | include-local-changes,缺失时安全回退为前者;后者只表达本次用户已同意纳入当前本地修改,不授权放宽文件、工具、冲突或规模门禁。trustedFastModeConsent: true 只有在本次请求选择 trusted-auto 且服务端已授权时才有效,模式、策略与同意字段不一致必须拒绝创建 Job。
工作区健康 endpoint 每次都从 Vite root 读取真实 Git 状态,不接受浏览器传入的路径或状态。staged、unstaged、untracked 和 conflicted 是可解释计数,同一文件同时 staged/unstaged 时 total 只计一次。未初始化仓库、进行中的 merge/rebase/cherry-pick/revert、未解决冲突、不支持的未跟踪项、未跟踪数量/体积超限分别返回稳定错误码;blocked 不得通过 include-local-changes 绕过。快照上限只由公共模型定义 (见 doc-id:03-public-api-models)。健康快照不是锁,创建 Job、Apply 和 Revert 必须再次复验 (见 doc-id:16-ai-agent-execution)。
多目标创建请求必须满足以下附加门禁:
- 只接受
SpotAnnotation.schemaVersion: 3;locale、targets和每个目标的instruction必须满足公共模型。每项说明 trim 后非空且不超过单项目标上限,说明总字符数不超过整组上限;协议拒绝缺项、未知字段、旧版全局note和超限数组,服务端再拒绝超过当前maxTargets配置的数组。版本、语言值域和全部上限只在公共模型定义 (见 doc-id:03-public-api-models)。 - 服务端按原顺序逐个授权目标。任一目标的 marker、
fileId、显示路径或 React source 不合法时整次请求失败,不创建部分 Job。 - 有完整 marker 的目标按
fileId + line + column去重;没有 marker 的完全相同目标也拒绝,防止重复上下文放大请求与费用。Runtime 去重不是服务端安全校验的替代。 - 浏览器提交的
CodeContext是不可信提示。Runtime 创建 Agent 请求时应省略它;即使恶意客户端提交,服务端也只校验路径一致性并从当前 Source Registry 重新读取每个目标的代码片段,Agent 只接收服务端结果。 - 授权时保留每个目标对应的说明,但浏览器提供的源码片段仍被替换为服务端当前读取结果;说明不授予额外文件、路径、命令或网络权限,也不能覆盖系统安全策略。
- 授权后的目标集作为一个不可变 Job 输入;不得合并不同目标说明、为“先成功的目标”创建子 Job,也不得在后续事件中追加或改写目标。
providerDataConsent: true 是客户端对本次创建请求的显式声明;缺失、false 或非布尔值均拒绝。服务端只把它记为当前 Vite 会话内对应 provider profile 的同意状态,不写盘、不跨会话继承,也不能用一次同意替代另一个 provider profile 的同意。能力探测不接收该字段且不得发送项目内容。
trustedFastModeConsent 缺失表示本次请求没有可信极速授权,只允许字面量 true,不得接受 false、字符串或持久化偏好。在服务端公开 trusted-auto 能力后,Runtime 默认保持 review;用户主动选择可信极速模式时,用一个明确说明后果的会话级复选框同时取得 provider 数据传输、纳入当前有界本地修改、跳过项目检查和直接应用的同意,并为每个 trusted-auto Job 重放该显式字段。切回 review、刷新页面、开发服务重启或切换 provider 后必须重新取得同意。该字段不放宽项目 root、保护文件、符号链接、文本/规模限制、任意 Shell、Git 操作状态、原子 patch 或基线哈希冲突。
Job ID 使用至少 128 bit 随机熵,只在当前 Vite 会话有效。Job endpoint 必须验证 token、Origin、Job 所属会话与允许的状态转换;未知 Job 对未授权请求统一表现为不可用,避免枚举。Job 事件、结果大小和保留时间受服务端上限控制,Vite 退出后全部失效。
Agent 的状态、结果和 limits 使用公共模型 (见 doc-id:03-public-api-models),工具与 Apply/Revert 行为见 Agent 执行规范 (见 doc-id:16-ai-agent-execution),外部 provider 协议和 capability 语义见模型提供商规范 (见 doc-id:17-model-provider-credentials)。
export const ERROR_CODES = {
INVALID_REQUEST: "INVALID_REQUEST",
INVALID_TOKEN: "INVALID_TOKEN",
ORIGIN_NOT_ALLOWED: "ORIGIN_NOT_ALLOWED",
SOURCE_NOT_FOUND: "SOURCE_NOT_FOUND",
SOURCE_OUTSIDE_ROOT: "SOURCE_OUTSIDE_ROOT",
SOURCE_TOO_LARGE: "SOURCE_TOO_LARGE",
EDITOR_OPEN_FAILED: "EDITOR_OPEN_FAILED",
DATA_FLOW_DISABLED: "DATA_FLOW_DISABLED",
DATA_FLOW_SOURCE_STALE: "DATA_FLOW_SOURCE_STALE",
DATA_FLOW_ANALYSIS_CANCELLED: "DATA_FLOW_ANALYSIS_CANCELLED",
AI_DISABLED: "AI_DISABLED",
PROVIDER_NOT_CONFIGURED: "PROVIDER_NOT_CONFIGURED",
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
PROVIDER_PROTOCOL_UNSUPPORTED: "PROVIDER_PROTOCOL_UNSUPPORTED",
MODEL_NOT_ALLOWED: "MODEL_NOT_ALLOWED",
MODEL_TOOL_CALL_UNSUPPORTED: "MODEL_TOOL_CALL_UNSUPPORTED",
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
AGENT_BUSY: "AGENT_BUSY",
AGENT_LIMIT_EXCEEDED: "AGENT_LIMIT_EXCEEDED",
AGENT_CANCELLED: "AGENT_CANCELLED",
WORKTREE_DIRTY: "WORKTREE_DIRTY",
WORKTREE_NOT_REPOSITORY: "WORKTREE_NOT_REPOSITORY",
WORKTREE_OPERATION_IN_PROGRESS: "WORKTREE_OPERATION_IN_PROGRESS",
WORKTREE_CONFLICTED: "WORKTREE_CONFLICTED",
WORKTREE_LOCAL_CHANGES_TOO_LARGE: "WORKTREE_LOCAL_CHANGES_TOO_LARGE",
WORKTREE_UNTRACKED_UNSUPPORTED: "WORKTREE_UNTRACKED_UNSUPPORTED",
WORKTREE_LOCAL_CHANGES_UNSUPPORTED: "WORKTREE_LOCAL_CHANGES_UNSUPPORTED",
TOOL_DENIED: "TOOL_DENIED",
TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
TOOL_ARGUMENTS_INVALID: "TOOL_ARGUMENTS_INVALID",
TOOL_CALL_ID_CONFLICT: "TOOL_CALL_ID_CONFLICT",
TOOL_PATH_DENIED: "TOOL_PATH_DENIED",
PATCH_REJECTED: "PATCH_REJECTED",
VALIDATION_FAILED: "VALIDATION_FAILED",
APPLY_CONFLICT: "APPLY_CONFLICT",
INTERNAL_ERROR: "INTERNAL_ERROR",
} as const;TOOL_INPUT_INVALID 只保留为旧客户端兼容码,新执行路径不得再用它混合不同根因。工具 JSON 不是对象、无法解析、缺字段、含未知字段、类型错误或超过字段约束时使用 TOOL_ARGUMENTS_INVALID;其中已解析对象的字段契约错误可以作为零副作用、一次性可重试工具结果返回,畸形 JSON 仍终止 Job。同一模型轮次内出现相同 toolCallId 且不能归一为同一流式调用时返回 TOOL_CALL_ID_CONFLICT。跨轮次复用 provider 原始 ID 是允许的,幂等和活动关联必须额外包含 SpotPatch turn。read_file 对保护、项目外、缺失、目录、符号链接、二进制或非 UTF-8 路径统一返回脱敏的 TOOL_PATH_DENIED 可恢复结果,不区分具体拒绝原因、不回显请求路径,也不读取或修改文件;模型只能改用 list_files / search_text 已返回的允许路径。任何写工具的路径拒绝仍终止 Job。patch 策略拒绝和 provider 流协议错误继续使用各自既有错误码,不得退化为上述通用诊断。
启动时先得到真实 root:
const realRoot = await fs.realpath(resolvedConfig.root);每次读取前重新解析真实文件路径并校验:
export async function assertInsideRoot(
root: string,
candidate: string,
): Promise<string> {
const [realRoot, realCandidate] = await Promise.all([
fs.realpath(root),
fs.realpath(candidate),
]);
const relative = path.relative(realRoot, realCandidate);
const outside = relative.startsWith(`..${path.sep}`)
|| relative === ".."
|| path.isAbsolute(relative);
if (outside) {
throw new SpotPatchError("SOURCE_OUTSIDE_ROOT");
}
return realCandidate;
}这样同时阻止 ../ 和符号链接逃逸。
- 只允许 registry 已登记文件。
- 只允许
.tsx、.jsx。 - 默认拒绝大于 1 MB 的源文件。
- maxLines 由服务端上限裁剪,不能信任客户端值。
- 文件变化或删除返回结构化错误,不回退到任意路径搜索。
Agent 文件工具只在隔离 worktree root 内操作普通文本文件,并在每次读写前后校验规范化路径、realpath、文件类型和大小。以下路径或类型在 v1.1 中无条件拒绝,配置、模型和用户请求都不能放宽:
.git/**、.env、.env.*。**/*.pem、**/*.key、**/id_rsa*及其他已识别的私钥/凭据文件。node_modules/**、dist/**、coverage/**和工具临时目录。- package manager lockfile、Git 子模块、符号链接、socket、device、FIFO 和二进制文件。
- root 外路径、绝对路径、含
../NUL 的路径、大小写或分隔符规范化后落入保护范围的路径。
Agent 允许范围不是 Source Registry 的扩大版:服务端从 worktree root 开始执行独立授权,结果只返回相对 POSIX 路径。任一变更触及保护路径、文件模式提权、规模上限或路径不确定性时,整批变更 fail-closed,不保留“其余看起来安全”的部分。
- token 至少 128 bit 随机熵。
- 每次 Vite 启动重新生成。
- token 不写入磁盘和日志。
- 客户端仅放在请求 header。
allowLan: false时拒绝非 loopback Host/Origin。- Agent endpoint 即使在
allowLan: true时也继续要求 token、精确 Origin 和首次 provider 数据传输同意;不得因为处于局域网而降低门禁。 - provider Key 只在 Node Secret Resolver 中解析;会话 token 与 provider Key 不得互换、记录或传给检查子进程。
浏览器使用 fileId 而不是路径的决策见 ADR-005 (见 doc-id:15-risks-adr)。
- 生产残留 = 0。
- 目录穿越与符号链接逃逸全部拒绝。
- 无 token、错误 token、错误 Origin 全部拒绝。
- 密码字段泄漏 = 0。
- 默认 Prompt 不包含 Cookie、Authorization 和存储 token。
- AI 禁用时 Agent endpoint、provider 请求、Git worktree 和检查子进程数量均为 0。
- 浏览器 bundle、虚拟模块、HTML、日志、错误、Job 事件和生产产物中的 provider Key 泄漏 = 0。
- 浏览器伪造 URL、模型、命令、root、路径和 apply mode 全部拒绝。
- 空目标、重复目标、超过协议或配置上限的目标集,以及任一目标的伪造 source/code 全部拒绝;不得把浏览器代码片段直接发送给 provider。
- Prompt injection、恶意工具参数、同轮冲突
toolCallId、超限输出和保护路径修改均 fail-closed;跨轮相同 ID 由内部turn安全隔离。 - 未取得显式同意的脏工作区、被阻断的 Git 状态、并发变化、检查失败和 Apply/Revert 哈希冲突均不得覆盖用户文件;已同意纳入的 staged、unstaged、untracked 修改及其 index 状态必须在 Apply/Revert 前后保持,撤销只移除 Agent 增量。
测试用例和 CI 门禁由测试与验收规范定义 (见 doc-id:12-testing-acceptance)。