Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,16 @@ jobs:
global-json-file: global.json
- name: Verify real cancellation transports without skips
run: bash scripts/gates/run-acp-cancellation-transport-gates.sh Release
- name: Upload cancellation evidence
- name: Verify bound credentials on real transports without skips
run: bash scripts/gates/run-acp-credential-transport-gates.sh Release
- name: Upload transport evidence
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: acp-cancellation-transports
path: artifacts/acp-cancellation/
path: |
artifacts/acp-cancellation/
artifacts/acp-credentials/

build-and-test:
name: Build and Test
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
- Transcript 视口事实源:当消息 transcript 需要在用户脱底、内容流式更新、会话热切回、冷进入或 overlay 暂停/恢复之间保持阅读位置时,follow/detached、per-conversation restore token、scroll request generation 和 restore lifecycle 必须由单一 Core controller 拥有;View 只采集原生 ListView viewport 事实并执行 native scroll/restore 意图;禁止 no-op 兼容 API、View-local overlay shadow flag、容器 realized 状态驱动 loading、或用 projection/list layout tick 推断业务状态;验证覆盖 following 内容追加、pinned 内容更新、warm return、cold enter、overlay 同/异会话恢复、Desktop/WASM 构建和真实 Skia Desktop GUI smoke。
- 后台恢复完成的权威晋升:当前台激活可被更新意图 supersede、而后台会话恢复(`session/load` / `resume`)在与激活解耦的 request token 上继续跑完时,被 supersede 的完成只要 binding、profile 与 connection instance identity 仍与该 conversation 匹配,就必须落 projection 并把 runtime 晋升为 authoritative Warm(`SessionLoadCompleted` / `SessionResumeCompleted`),使回切成为零往返 warm reuse 而非再次慢恢复;晋升工作必须按 conversationId 隔离,禁止触碰前台(`HydratedConversationId` / overlay / 选中态由最新激活拥有)。唯一例外:若**同一** conversation 已有更新的在途激活(runtime 已被重置为 `Selecting` / `Selected` / `RemoteConnectionReady` 等更早 pending 阶段),旧的后台完成不得晋升 Warm 或落正文,须让更新激活驱动自己的权威恢复。禁止做法:superseded 完成分支只清 buffering 而不晋升(会让掠过的会话永远停在 `RemoteHydrating`,每次回切都 `RuntimeStateNotWarm` 重跑慢恢复);也禁止无条件晋升而忽略同会话更新激活。验证覆盖:不同会话 supersede 后旧会话晋升 Warm 且前台不被夺、同会话更新激活在途时旧完成不晋升且新激活重跑恢复、binding/connection identity 不匹配时不晋升。
- 测试缝隙与风险归属:在判定一个逻辑类型“无法被干净测试”或“不值得测”之前,必须先排查既有测试缝隙(projector / factory / `.Empty` / 带默认值的 record / adapter / 测试工程已有 mock 模式),不得仅凭“构造参数多”“看起来是框架胶水”就下结论并推迟覆盖;测试对象按风险归属划分,框架与运行时保证的能力(如 `.WaitAsync` 计时、record 结构 `Equals` 等语言契约)不重复测,应用层接线、封装、诊断文案、null 缝、适配层、状态机顺序与门控才是测试对象,断言须复用被测类的契约常量(如 policy 常量)而非魔法数,且不重复其下层单元测试已覆盖的功劳;耦合若是有意设计(如全字段结构相等以避免“字段变了也抑制”的行为退化),测试须基于真实实例构造而非缩小相等面或改生产契约;验证覆盖构造路径确认(`.Empty` / 默认值 / mock seam 是否能造出真实实例)、状态机各分支与门控、null/空边界、诊断渲染与内层异常保留,以及新测试用例数与实际执行数一致。
- 应用启动副作用所有权:当 singleton 运行态需要加载配置、恢复 workspace、重建导航树或自动连接时,必须由 application-scoped startup workflow 统一触发并共享在途任务;Page / Window 的 `Loaded` 只附着原生视图、焦点、viewport 与事件,不得直接加载 profile、恢复 conversation、另起 fire-and-forget 初始化或成为失败重试 owner。原生期望行为是 shell 首帧先完成挂载,运行态初始化随后由同一 owner 推进;失败只重试失败的子任务,不重复已完成恢复。验证覆盖冷启动、shell reload、Mini Window 首开、多个页面并发挂载、空配置目录、初始化失败重试与每个副作用调用次数。
- 应用启动副作用所有权:当 singleton 运行态需要加载配置、恢复 workspace、重建导航树或自动连接时,必须由 application-scoped startup workflow 统一触发并共享在途任务;Page / Window 的 `Loaded` 只附着原生视图、焦点、viewport 与事件,不得直接加载 profile、恢复 conversation、另起 fire-and-forget 初始化或成为失败重试 owner。原生期望行为是 shell 首帧先完成挂载,运行态初始化随后由同一 owner 推进;失败只重试失败的子任务,不重复已完成恢复。排队的初始聚焦必须在创建时快照已有导航 token、原生焦点元素/状态/CorrelationId 与 root,并在实际执行前确认仍有效;用户已进入内容区或产生新焦点/导航意图时放弃,禁止晚到的初始聚焦抢走输入,禁止另建 focus generation owner。验证覆盖冷启动、shell reload、Mini Window 首开、多个页面并发挂载、空配置目录、初始化失败重试与每个副作用调用次数;焦点门禁覆盖输入途中回调、离开后回到同一元素、root 卸载和无用户操作的初始聚焦,并以真实按键完整值及撤掉保护时失败的反向证据验收
- 回收型容器的 per-container 状态与集合抖动:当控件把选中/焦点/展开等状态存在**被回收复用的容器**上(`ItemsRepeater` 家族,含 `NavigationView`、`ListView`、`ItemsView` 的虚拟化宿主)时,应用层在该状态存活期间不得搬动已渲染行。触发条件:绑定集合会因 recency/活跃度等易变排序键在用户交互或加载风暴期间重排。原生期望行为:把「已渲染行的位置」当作控件借出的资源——插入/删除安全(宿主只对存活元素重编索引,仅回收被删数据自身的容器),搬动不安全(`Move` 被 `ItemsRepeater` 拆成 Remove+Add,容器进回收池且池不复位 per-container 状态,父控件「取消上一个选中」在容器已非 realized 时会静默 no-op),故未静默期只保持既有顺序并追加新行,静默后再收敛到目标顺序。禁止做法:用 code-behind 遍历 realized 容器强清 `IsSelected` 等控件自有视觉状态(与控件自身对账竞态,且若真凶是 pointer 态则完全无效);仅改集合事件形状(`Remove`+`Insert` 换成 `Move`)就认为保住了状态——WinUI 文档只承诺 `INotifyCollectionChanged` 能投递真 `Move`,不承诺宿主不拆解它;为保住某一行而重排其余所有行(总变更次数才是主因子,附带 churn 会把一个脏容器扩散成多行残留)。上游已修但未进当前依赖线时,必须在代码注释与提交信息中记录 issue/PR 号与「上游落地后删除本缓解」的条件。验证覆盖:快速连续切换叠加高频集合重排、加载期与静默期两种路径、收敛不饿死,以及在**真实控件树**上枚举全部 realized 容器断言该状态数量不超过控件契约上限。
- 对端违反协议时的归责与呈现:当规范把某条约束落在**对端**(例:"The agent MUST NOT write anything to its `stdout` that is not a valid ACP message",诊断输出应走 `stderr`),而对端仍然违反时,client 不得把解析器/校验器的原始报错当作用户可见错误呈现——那既非用户所为、也无可操作性,落在会「保持到下一次成功操作」的故障面上还会形成滞留提示。触发条件:规范对该流/字段有 MUST 级约束且明确指定了合法去处,而收到的输入根本不属于协议消息。原生期望行为:先把**判据**与**归因**分开——"这是不是一条协议消息"是协议层事实,必须收敛为单一定义供所有传输共用;"这属于对端写错了流"才是某一层独有的归因(如 stdout 与 stderr 之分只有 stdio 传输层看得见,WebSocket/HTTP 无此二分)。**把判据也收进那一层是本条最易犯的错**:一旦存在中转(stdio→WS 桥原样转发对端 stdout),未设判据的那条路会把同一条非协议输入照原样送达并复现缺陷,而"某传输已过滤"的注释会掩盖它。非协议输入按已有的"对端诊断"通路处理(记日志后早退,与既有 stderr 处理同形),并把内容与前若干字节 hex 记到**默认可见的级别**——同一条解析器消息可能对应 BOM、`U+FFFD`、私有区字形、纯日志等多种成因,只有前导字节能区分,仅记长度等于无法定位。已成帧但解析失败的才是真协议错误,按 JSON-RPC 2.0 回 `-32700` 且 id 必须为**显式 null**(若信封序列化设了 `WhenWritingNull`,null id 会被整个丢掉从而降级成 Notification,须对该属性做属性级覆盖并用线上文本断言);从来不像帧的行不回 `-32700`——它没有请求可回应,且实测会触发"回复自身发送失败"的二次错误。宽容仅限规范明确许可处(如 RFC 8259 §8.1 允许解析方 "ignore the presence of a byte order mark rather than treating it as an error",故前导 BOM 应剥除而非拒绝;注意 `U+FEFF` 不被 `IsNullOrWhiteSpace` 视为空白,纯 BOM 行会穿过空行守卫)。禁止做法:把对端违规呈现为 client 故障;仅记长度不记内容与 hex;用 Debug 级记违规(默认级别看不见);对非帧行回 `-32700`;把宽容扩大到规范未许可处(如放宽必填字段或类型契约)。验证覆盖:**每一种在用传输**各自跑一遍(含经桥中转的那条,否则半个修复会被另一半的注释掩盖)、真实子进程在真实管道上发出违规字节(注意 `dash` 的 `printf %b` 不认 `\xEF` 十六进制转义、须用八进制,且特殊字符经源文件编码可能丢失,宜在脚本侧生成)、合规对端零违规且帧原样通行、跨传输回归(共享层改动对 WebSocket/HTTP 同样生效)、以及从传输到 client 事件的**整条链**断言不向用户抛错。
- 视觉树级缺陷的运行时门禁与反向验证:当缺陷只在真实控件树上可观测(容器回收、视觉状态、焦点/选中投影、布局回流)时,单元测试不构成安全网,必须建立运行时门禁:由**视图层**只读采集原生事实(枚举 realized 容器及其状态)并输出可断言标记,由**独立诊断组件**自持依赖、按环境变量自我门控地施加负载,门禁脚本对不变式做硬断言而非仅打印数值。禁止做法:让页面 code-behind 直接驱动下层服务制造负载(分层异味);门禁只 `echo` 指标由人肉判读;断言 mock 调用或集合事件形状而非用户可观测的原生状态(会给出假信心);把不可靠的观测字段(跨行同名模板部件、模板作用域视觉状态组)当证据。新增或修改此类门禁后必须做**反向验证**:临时移除修复并确认门禁失败(记录失败输出),否则不得认为门禁有效。验证覆盖:修复在位时多轮稳定通过、修复移除时必然失败、采样数下限校验以防空跑绿。
Expand Down
23 changes: 23 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ printf '%s\n' "$AGENT_TOKEN" | salmon-egg set-credential <server-id> --token-std
printf '%s\n' "$AGENT_TOKEN" | salmon-egg --allow-insecure-storage set-credential <server-id> --token-stdin
```

#### Credential injection

Stored credentials need an explicit injection binding. Use the environment variable or header name
documented by the Agent. A binding approves the current stdio command and arguments, or the complete
network endpoint; editing that destination requires binding it again.

```bash
# Inject an API key only into the configured stdio child process.
salmon-egg config server update <server-id> --credential-source api_key --credential-env AGENT_API_KEY

# Send Authorization: Bearer ... to the configured HTTP or desktop WebSocket endpoint.
salmon-egg config server update <server-id> --credential-source token --credential-header Authorization --credential-scheme Bearer

# Remove injection while retaining the stored credential.
salmon-egg config server update <server-id> --clear-credential-binding
```

The app's profile editor exposes the same binding. Empty credential fields preserve the stored value;
the explicit clear option removes it. Saved changes take effect on reconnect. A binding with a missing
credential prevents connection until the credential is set or the binding is removed. Browser WebSocket
cannot set request headers; use an HTTP endpoint supported by the Agent. Bound network requests do not
follow redirects to prevent forwarding credentials to another destination.

#### Running from source

The examples below use `dotnet run` so they work in a checkout without installing anything. Replace the `dotnet run --project ... --` prefix with `salmon-egg` when using an installed build.
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ printf '%s\n' "$AGENT_TOKEN" | salmon-egg --allow-insecure-storage set-credentia

凭据值只从 stdin 读取,不会进入进程参数、YAML 或 `has-credential` 输出。非凭据配置操作不受该策略影响。该策略针对 Linux Secret Service 与 macOS Keychain;Windows DPAPI 始终可用,该 flag 在 Windows 上无实际作用。完整命令示例见 [README.en.md](README.en.md#cli-configuration-management),发布与安装细节见 [发布指南](docs/release-guide.md#cli-发布)。

保存凭据后,还需按 Agent 文档指定发送位置。绑定只对当前启动命令或完整网络地址生效;更改目标后必须重新绑定。示例:

```bash
# 本地 Agent:仅注入其子进程环境
salmon-egg config server update <server-id> --credential-source api_key --credential-env AGENT_API_KEY

# HTTP / 桌面 WebSocket:向当前地址发送 Authorization: Bearer ...
salmon-egg config server update <server-id> --credential-source token --credential-header Authorization --credential-scheme Bearer

# 停止注入,保留已保存的凭据
salmon-egg config server update <server-id> --clear-credential-binding
```

应用内的配置编辑页也可设置“凭据发送方式”。凭据输入留空会保留旧值,勾选清除才会删除;保存后的更改在重新连接时生效。绑定仍在但凭据已清除时,连接会提示补充凭据或移除绑定。浏览器 WebSocket 无法设置请求头,须使用 Agent 支持的 HTTP 接口。绑定的网络请求不跟随重定向,避免凭据转发到另一个目标。

## 文档

- [文档导航](docs/README.md)
Expand Down
Loading
Loading