dsh-plugin-dev-kb 1.0.0

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.
Files changed (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/cordis.patch.yml +12 -0
  4. package/kb/INDEX.md +210 -0
  5. package/kb/README.md +69 -0
  6. package/kb/extra/AGENTS.md +75 -0
  7. package/kb/extra/api-gateway.md +164 -0
  8. package/kb/extra/api-gateway.zh.md +164 -0
  9. package/kb/extra/cookbook/adding-a-vendored-package.md +59 -0
  10. package/kb/extra/cookbook/adding-a-vendored-package.zh.md +59 -0
  11. package/kb/extra/cookbook/maintaining-dsh-code-review.md +64 -0
  12. package/kb/extra/cookbook/maintaining-dsh-code-review.zh.md +64 -0
  13. package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.md +32 -0
  14. package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.zh.md +32 -0
  15. package/kb/extra/defensive-patterns.md +33 -0
  16. package/kb/extra/defensive-patterns.zh.md +33 -0
  17. package/kb/extra/development.md +171 -0
  18. package/kb/extra/development.zh.md +171 -0
  19. package/kb/extra/event-producer-consumer.md +76 -0
  20. package/kb/extra/event-producer-consumer.zh.md +78 -0
  21. package/kb/extra/glossary.md +45 -0
  22. package/kb/extra/glossary.zh.md +45 -0
  23. package/kb/extra/graph-atlas.md +24 -0
  24. package/kb/extra/graph-atlas.zh.md +26 -0
  25. package/kb/extra/i18n/README.md +60 -0
  26. package/kb/extra/i18n/README.zh.md +60 -0
  27. package/kb/extra/i18n/style-samples.md +87 -0
  28. package/kb/extra/i18n/terminology.md +214 -0
  29. package/kb/extra/i18n/translation-prompt.md +263 -0
  30. package/kb/extra/i18n/translation-rules.md +69 -0
  31. package/kb/extra/i18n/translation-rules.zh.md +69 -0
  32. package/kb/extra/module-graph.md +1641 -0
  33. package/kb/extra/module-graph.zh.md +1643 -0
  34. package/kb/extra/postmortem/0001-acp-default-export-drops-inject.md +113 -0
  35. package/kb/extra/postmortem/0001-acp-default-export-drops-inject.zh.md +113 -0
  36. package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.md +47 -0
  37. package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md +47 -0
  38. package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.md +53 -0
  39. package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.zh.md +53 -0
  40. package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md +55 -0
  41. package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md +55 -0
  42. package/kb/extra/postmortem/README.md +18 -0
  43. package/kb/extra/postmortem/README.zh.md +18 -0
  44. package/kb/extra/rescope.md +53 -0
  45. package/kb/extra/rescope.zh.md +53 -0
  46. package/kb/extra/subsystems/attachment.md +125 -0
  47. package/kb/extra/subsystems/attachment.zh.md +125 -0
  48. package/kb/extra/subsystems/extensions.md +364 -0
  49. package/kb/extra/subsystems/extensions.zh.md +364 -0
  50. package/kb/extra/subsystems/feedback.md +266 -0
  51. package/kb/extra/subsystems/feedback.zh.md +266 -0
  52. package/kb/extra/testing.md +49 -0
  53. package/kb/extra/testing.zh.md +49 -0
  54. package/kb/extra/web-styling.md +25 -0
  55. package/kb/extra/web-styling.zh.md +25 -0
  56. package/kb/meta/search-index.json +1328 -0
  57. package/kb/meta/site-pages.txt +168 -0
  58. package/kb/meta/source.json +13 -0
  59. package/kb/meta/topics.md +75 -0
  60. package/kb/site/develop/basic/config.md +108 -0
  61. package/kb/site/develop/basic/index.md +146 -0
  62. package/kb/site/develop/basic/publish.md +185 -0
  63. package/kb/site/develop/basic/tool.md +54 -0
  64. package/kb/site/develop/cordis-tutorial/01-first-plugin.md +95 -0
  65. package/kb/site/develop/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
  66. package/kb/site/develop/cordis-tutorial/03-services.md +98 -0
  67. package/kb/site/develop/cordis-tutorial/04-events.md +144 -0
  68. package/kb/site/develop/cordis-tutorial/05-config.md +84 -0
  69. package/kb/site/develop/cordis-tutorial/06-composition-and-hmr.md +113 -0
  70. package/kb/site/develop/cordis-tutorial/07-into-the-harness.md +107 -0
  71. package/kb/site/develop/cordis-tutorial/index.md +62 -0
  72. package/kb/site/develop/framework/events.md +145 -0
  73. package/kb/site/develop/framework/index.md +139 -0
  74. package/kb/site/develop/framework/service.md +152 -0
  75. package/kb/site/develop/practice/index.md +157 -0
  76. package/kb/site/develop/practice/llm-adapter.md +190 -0
  77. package/kb/site/en/develop/basic/config.md +108 -0
  78. package/kb/site/en/develop/basic/index.md +146 -0
  79. package/kb/site/en/develop/basic/publish.md +185 -0
  80. package/kb/site/en/develop/basic/tool.md +54 -0
  81. package/kb/site/en/develop/cordis-tutorial/01-first-plugin.md +95 -0
  82. package/kb/site/en/develop/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
  83. package/kb/site/en/develop/cordis-tutorial/03-services.md +98 -0
  84. package/kb/site/en/develop/cordis-tutorial/04-events.md +144 -0
  85. package/kb/site/en/develop/cordis-tutorial/05-config.md +84 -0
  86. package/kb/site/en/develop/cordis-tutorial/06-composition-and-hmr.md +113 -0
  87. package/kb/site/en/develop/cordis-tutorial/07-into-the-harness.md +107 -0
  88. package/kb/site/en/develop/cordis-tutorial/index.md +60 -0
  89. package/kb/site/en/develop/framework/events.md +145 -0
  90. package/kb/site/en/develop/framework/index.md +139 -0
  91. package/kb/site/en/develop/framework/service.md +150 -0
  92. package/kb/site/en/develop/practice/index.md +157 -0
  93. package/kb/site/en/develop/practice/llm-adapter.md +190 -0
  94. package/kb/site/en/guide/providers-custom-form.png +0 -0
  95. package/kb/site/en/guide/providers-models-page.png +0 -0
  96. package/kb/site/en/guide/providers.md +100 -0
  97. package/kb/site/en/guide/python-sdk.md +106 -0
  98. package/kb/site/en/guide/quickstart.md +32 -0
  99. package/kb/site/en/index.md +8 -0
  100. package/kb/site/en/reference/agent-lifecycle.md +86 -0
  101. package/kb/site/en/reference/capability-seams.md +475 -0
  102. package/kb/site/en/reference/config-catalog.md +3155 -0
  103. package/kb/site/en/reference/cookbook/adding-a-conversation-node.md +235 -0
  104. package/kb/site/en/reference/cookbook/adding-a-package.md +120 -0
  105. package/kb/site/en/reference/cookbook/adding-a-settings-card.md +102 -0
  106. package/kb/site/en/reference/cookbook/adding-a-tool.md +96 -0
  107. package/kb/site/en/reference/cookbook/adding-an-llm-adapter.md +45 -0
  108. package/kb/site/en/reference/cookbook/extension-cookbook.md +131 -0
  109. package/kb/site/en/reference/cordis-api/context.md +368 -0
  110. package/kb/site/en/reference/cordis-api/events.md +211 -0
  111. package/kb/site/en/reference/cordis-api/fiber.md +379 -0
  112. package/kb/site/en/reference/cordis-api/inherited.md +43 -0
  113. package/kb/site/en/reference/cordis-api/registry.md +156 -0
  114. package/kb/site/en/reference/cordis-api/service.md +106 -0
  115. package/kb/site/en/reference/cordis-primer.md +46 -0
  116. package/kb/site/en/reference/index.md +131 -0
  117. package/kb/site/en/reference/persistence-catalog.md +949 -0
  118. package/kb/site/en/reference/subsystems/approval.md +173 -0
  119. package/kb/site/en/reference/subsystems/client-modules.md +121 -0
  120. package/kb/site/en/reference/subsystems/code-runtime.md +194 -0
  121. package/kb/site/en/reference/subsystems/commands.md +190 -0
  122. package/kb/site/en/reference/subsystems/compaction.md +241 -0
  123. package/kb/site/en/reference/subsystems/core.md +1073 -0
  124. package/kb/site/en/reference/subsystems/credentials.md +136 -0
  125. package/kb/site/en/reference/subsystems/filesystem.md +498 -0
  126. package/kb/site/en/reference/subsystems/goal.md +280 -0
  127. package/kb/site/en/reference/subsystems/index.md +58 -0
  128. package/kb/site/en/reference/subsystems/invariants.md +91 -0
  129. package/kb/site/en/reference/subsystems/jobs.md +293 -0
  130. package/kb/site/en/reference/subsystems/llm-streaming.md +920 -0
  131. package/kb/site/en/reference/subsystems/lsp.md +205 -0
  132. package/kb/site/en/reference/subsystems/permission-presets.md +134 -0
  133. package/kb/site/en/reference/subsystems/persistence.md +388 -0
  134. package/kb/site/en/reference/subsystems/plan.md +90 -0
  135. package/kb/site/en/reference/subsystems/sandbox.md +221 -0
  136. package/kb/site/en/reference/subsystems/schedule.md +189 -0
  137. package/kb/site/en/reference/subsystems/scope.md +62 -0
  138. package/kb/site/en/reference/subsystems/session-projection.md +265 -0
  139. package/kb/site/en/reference/subsystems/session-query.md +498 -0
  140. package/kb/site/en/reference/subsystems/session-reference.md +111 -0
  141. package/kb/site/en/reference/subsystems/session-telemetry.md +197 -0
  142. package/kb/site/en/reference/subsystems/session-title.md +207 -0
  143. package/kb/site/en/reference/subsystems/session.md +852 -0
  144. package/kb/site/en/reference/subsystems/settings.md +313 -0
  145. package/kb/site/en/reference/subsystems/shell.md +306 -0
  146. package/kb/site/en/reference/subsystems/skills.md +334 -0
  147. package/kb/site/en/reference/subsystems/spill.md +120 -0
  148. package/kb/site/en/reference/subsystems/storage.md +232 -0
  149. package/kb/site/en/reference/subsystems/subagent.md +737 -0
  150. package/kb/site/en/reference/subsystems/subprocess.md +327 -0
  151. package/kb/site/en/reference/subsystems/system-prompt.md +210 -0
  152. package/kb/site/en/reference/subsystems/terminal.md +187 -0
  153. package/kb/site/en/reference/subsystems/token-meter.md +93 -0
  154. package/kb/site/en/reference/subsystems/tools.md +723 -0
  155. package/kb/site/en/reference/subsystems/typert.md +339 -0
  156. package/kb/site/en/reference/subsystems/user-questions.md +181 -0
  157. package/kb/site/en/reference/subsystems/web-server.md +111 -0
  158. package/kb/site/en/reference/subsystems/web.md +202 -0
  159. package/kb/site/en/reference/subsystems/workflow.md +281 -0
  160. package/kb/site/en/reference/subsystems/workspace.md +231 -0
  161. package/kb/site/en/reference/tool-catalog.md +1877 -0
  162. package/kb/site/en/reference/tool-execution-pipeline.md +66 -0
  163. package/kb/site/guide/providers-custom-form.zh.png +0 -0
  164. package/kb/site/guide/providers-models-page.zh.png +0 -0
  165. package/kb/site/guide/providers.md +100 -0
  166. package/kb/site/guide/python-sdk.md +106 -0
  167. package/kb/site/guide/quickstart.md +32 -0
  168. package/kb/site/index.md +8 -0
  169. package/kb/site/reference/agent-lifecycle.md +86 -0
  170. package/kb/site/reference/capability-seams.md +475 -0
  171. package/kb/site/reference/config-catalog.md +3154 -0
  172. package/kb/site/reference/cookbook/adding-a-conversation-node.md +235 -0
  173. package/kb/site/reference/cookbook/adding-a-package.md +120 -0
  174. package/kb/site/reference/cookbook/adding-a-settings-card.md +102 -0
  175. package/kb/site/reference/cookbook/adding-a-tool.md +98 -0
  176. package/kb/site/reference/cookbook/adding-an-llm-adapter.md +45 -0
  177. package/kb/site/reference/cookbook/extension-cookbook.md +133 -0
  178. package/kb/site/reference/cordis-api/context.md +368 -0
  179. package/kb/site/reference/cordis-api/events.md +211 -0
  180. package/kb/site/reference/cordis-api/fiber.md +379 -0
  181. package/kb/site/reference/cordis-api/inherited.md +43 -0
  182. package/kb/site/reference/cordis-api/registry.md +156 -0
  183. package/kb/site/reference/cordis-api/service.md +106 -0
  184. package/kb/site/reference/cordis-primer.md +52 -0
  185. package/kb/site/reference/index.md +135 -0
  186. package/kb/site/reference/persistence-catalog.md +949 -0
  187. package/kb/site/reference/subsystems/approval.md +173 -0
  188. package/kb/site/reference/subsystems/client-modules.md +121 -0
  189. package/kb/site/reference/subsystems/code-runtime.md +194 -0
  190. package/kb/site/reference/subsystems/commands.md +190 -0
  191. package/kb/site/reference/subsystems/compaction.md +241 -0
  192. package/kb/site/reference/subsystems/core.md +1081 -0
  193. package/kb/site/reference/subsystems/credentials.md +136 -0
  194. package/kb/site/reference/subsystems/filesystem.md +498 -0
  195. package/kb/site/reference/subsystems/goal.md +280 -0
  196. package/kb/site/reference/subsystems/index.md +58 -0
  197. package/kb/site/reference/subsystems/invariants.md +91 -0
  198. package/kb/site/reference/subsystems/jobs.md +293 -0
  199. package/kb/site/reference/subsystems/llm-streaming.md +926 -0
  200. package/kb/site/reference/subsystems/lsp.md +205 -0
  201. package/kb/site/reference/subsystems/permission-presets.md +134 -0
  202. package/kb/site/reference/subsystems/persistence.md +388 -0
  203. package/kb/site/reference/subsystems/plan.md +90 -0
  204. package/kb/site/reference/subsystems/sandbox.md +221 -0
  205. package/kb/site/reference/subsystems/schedule.md +189 -0
  206. package/kb/site/reference/subsystems/scope.md +62 -0
  207. package/kb/site/reference/subsystems/session-projection.md +265 -0
  208. package/kb/site/reference/subsystems/session-query.md +498 -0
  209. package/kb/site/reference/subsystems/session-reference.md +111 -0
  210. package/kb/site/reference/subsystems/session-telemetry.md +197 -0
  211. package/kb/site/reference/subsystems/session-title.md +207 -0
  212. package/kb/site/reference/subsystems/session.md +854 -0
  213. package/kb/site/reference/subsystems/settings.md +313 -0
  214. package/kb/site/reference/subsystems/shell.md +306 -0
  215. package/kb/site/reference/subsystems/skills.md +334 -0
  216. package/kb/site/reference/subsystems/spill.md +120 -0
  217. package/kb/site/reference/subsystems/storage.md +232 -0
  218. package/kb/site/reference/subsystems/subagent.md +739 -0
  219. package/kb/site/reference/subsystems/subprocess.md +327 -0
  220. package/kb/site/reference/subsystems/system-prompt.md +210 -0
  221. package/kb/site/reference/subsystems/terminal.md +187 -0
  222. package/kb/site/reference/subsystems/token-meter.md +93 -0
  223. package/kb/site/reference/subsystems/tools.md +723 -0
  224. package/kb/site/reference/subsystems/typert.md +339 -0
  225. package/kb/site/reference/subsystems/user-questions.md +181 -0
  226. package/kb/site/reference/subsystems/web-server.md +111 -0
  227. package/kb/site/reference/subsystems/web.md +202 -0
  228. package/kb/site/reference/subsystems/workflow.md +281 -0
  229. package/kb/site/reference/subsystems/workspace.md +231 -0
  230. package/kb/site/reference/tool-catalog.md +1880 -0
  231. package/kb/site/reference/tool-execution-pipeline.md +66 -0
  232. package/package.json +40 -0
  233. package/scripts/rebuild-index.mjs +88 -0
  234. package/skills/dsh-plugin-dev-kb.md +66 -0
@@ -0,0 +1,113 @@
1
+ # Post-mortem 0001: ACP server crashed on connect — `export default` dropped the plugin's `inject`
2
+
3
+ English | [中文](0001-acp-default-export-drops-inject.zh.md)
4
+
5
+ Status: resolved (fix in PR #41 `feat/acp-2-bridge`)
6
+
7
+ ## Executive summary
8
+
9
+ Two integration mistakes broke ACP despite full unit coverage: a default export caused the Loader to discard `inject`, and a traced optional-service lookup failed across a shadow boundary. Hand-mounted tests bypassed both paths. The fixes added keyless real-Loader coverage and package rules for plugin exports and optional-service access.
10
+
11
+ ## Summary
12
+
13
+ The ACP server (`examples/acp-agent`, `@deepseek-ai/dsh-acp`) crashed the instant a real editor (Zed) connected: the first `session/new` request returned `Internal error: cannot get property "agents" without inject`, and `session/load` returned the same for `sessionPersistence`. The bridge was completely non-functional in production despite 178 green unit tests and 100% line coverage. Two independent bugs were hiding behind the same error string, and the test suite missed both for the same reason: every test mounted the plugin through a path that did not exercise how it actually loads or how its services actually resolve.
14
+
15
+ ## Impact
16
+
17
+ The ACP server could not create or load a single session — the two RPCs an editor calls first. Anyone wiring the agent into Zed got an immediate hard failure. No data loss (nothing persisted before the crash); the cost was entirely "the feature does not work" plus the debugging time to find out why, twice.
18
+
19
+ ## Timeline
20
+
21
+ - The bridge (RFC 010) landed with a full unit suite for the codec, in-memory transport, generated protocol messages, failure paths, and HMR; a key-gated real-API e2e; and a no-key stdout-purity e2e. All green, 100% coverage.
22
+ - A real Zed session immediately failed on `session/new` with `cannot get property "agents" without inject`.
23
+ - Investigation initially pursued a Cordis "traceable/shadow" theory (plausible, and the mechanism is real — see Bug #2), then instrumented the actual fiber walk in vendored `reflect.ts` and ran the real subprocess. The trace showed the throw at `apply()` line 179 *at plugin load time*, on the ROOT fiber with no shadow — falsifying the shadow theory for `session/new`.
24
+ - Root cause #1 found: a stray `export default apply`. Removing it fixed `session/new`.
25
+ - Removing it then exposed Bug #2: `session/load` still threw on `sessionPersistence` — a genuinely distinct mechanism (the shadow walk), confirmed by isolating the fix and re-running the real subprocess.
26
+
27
+ ## Root cause #1 — `export default apply` drops the plugin's `inject` (broke `session/new`)
28
+
29
+ `packages/acp/acp/src/index.ts` is a *namespace plugin*: it exports `name`, `inject`, `Config`, and `apply` as separate named exports, as every other plugin in the repo does (`invariants`, `llm-deepseek`, `tool-bash`, `tui`, …). But it *also* ended with one extra line no other plugin had:
30
+
31
+ ```ts ignore-check
32
+ export const name = 'acp'
33
+ export const inject = ['agents', 'sessions', 'sessionPersistence']
34
+ export function apply(ctx: Context, config: AcpConfig): void { /* … */ }
35
+ // …
36
+ export default apply // ← the bug
37
+ ```
38
+
39
+ When a plugin is loaded from `cordis.yml`, the cordis Loader normalizes the imported module through `Loader.unwrapExports` (`vendor/loader/src/index.ts`):
40
+
41
+ ```ts ignore-check
42
+ unwrapExports(exports: any) {
43
+ if (isNullable(exports)) return exports
44
+ exports = exports.default ?? exports // ← prefers `.default`
45
+ if (!exports.__esModule) return exports
46
+ return exports.default ?? exports
47
+ }
48
+ ```
49
+
50
+ With a default export present, `exports.default ?? exports` resolves to the **bare `apply` function**. A bare function has no `inject`, no `name`, no `Config` properties — those lived as *sibling* named exports on the module namespace, and unwrapping to `.default` threw the namespace away. The Loader then built the plugin's fiber from an empty `inject`.
51
+
52
+ Consequently `apply` ran in a fiber with **no injected services**. The very first line, `const agents = ctx.agents`, walked the fiber tree (ROOT → Include → Loader → ROOT) and, finding `agents` in no fiber's store and reaching the root fiber (`runtime === null`), threw `cannot get property "agents" without inject`. The crash was at *load time*, not in a later request handler — the request just happened to be what triggered the load in the failing trace.
53
+
54
+ **Fix:** delete `export default apply`. The Loader then uses the module namespace, honors `inject`/`name`/`Config`, and `apply` runs inside a fiber that actually grants the declared services.
55
+
56
+ ## Root cause #2 — optional service read trips the inject guard through a traceable shadow (broke `session/load`)
57
+
58
+ With #1 fixed, `session/new` worked but `session/load` still threw `cannot get property "sessionPersistence" without inject`. This one *is* the Cordis traceable/shadow mechanism, and it is worth understanding precisely.
59
+
60
+ `session/load` calls `agents.resume(...)`, which delegates to `AgentLoop.resume()`, which read `this.ctx.sessionPersistence`. `AgentLoop`'s `static inject` deliberately does NOT include `sessionPersistence` — injecting it would make non-persistent demos pend forever waiting for a backend that never loads. The service is provided by a separate sibling plugin/fiber and read opportunistically.
61
+
62
+ Service access in Cordis goes through a context proxy (`vendor/cordis/src/reflect.ts`). When a service method is invoked through a *traceable proxy* obtained from a foreign fiber (here: the bridge fiber calls `ctx.agents.resume`, and the registry hands back `this.factory` — the `AgentLoop` — re-wrapped as a fresh traceable proxy bound to the caller), `createShadowMethod` (`vendor/cordis/src/utils.ts`) rebinds `this` to a *shadow* object whose `ctx` carries `[symbols.shadow]` pointing at `AgentLoop`'s own construction context. Inside `resume`, then, `this.ctx.sessionPersistence` resolves with the proxy handler starting its fiber walk from the shadow's fiber:
63
+
64
+ ```ts ignore-check
65
+ // reflect.ts get handler
66
+ let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber // ← starts at AgentLoop's fiber
67
+ while (true) {
68
+ const impl = fiber.store?.[prop]
69
+ if (impl) return getTraceable(ctx, impl.value)
70
+ if (prop in fiber.inject) { /* inactive-context error */ }
71
+ if (!fiber.runtime) throw error // ← reached root, throw
72
+ if (fiber.parent[symbols.isolate][prop] !== key) throw error
73
+ fiber = fiber.parent.fiber // ← ancestor-only
74
+ }
75
+ ```
76
+
77
+ The walk is **ancestor-only**. `sessionPersistence` is in neither `AgentLoop`'s fiber store (not in its `static inject`) nor any ancestor on the way to root (it lives on a *sibling* branch), so the walk reaches the root fiber and throws.
78
+
79
+ Why didn't the in-memory `AgentLoop` resume tests catch this? Because they call `ctx.agents.resume(...)` directly from test code — *outside any plugin fiber*. There, `ctx.fiber.runtime` is `null`, so the proxy handler takes an early bypass:
80
+
81
+ ```ts ignore-check
82
+ if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false) // ← direct global-store lookup, no fiber walk
83
+ ```
84
+
85
+ `ctx.reflect.get(name, false)` is a direct lookup in the global service store keyed by the isolate symbol — it ignores fiber topology entirely and finds the service. So from a top-level test the read works; from inside a real plugin fiber, reached via a shadow, it throws. The bridge is exactly the latter.
86
+
87
+ **Fix:** read the optional service with `ctx.get('sessionPersistence')`, which uses the global isolate-keyed store while preserving active-state checks. Direct property reads remain appropriate for services in the plugin's declared injection set.
88
+
89
+ ## Why every test missed it (the real failure)
90
+
91
+ Both bugs share one root process gap: **no test exercised the plugin through its real load path or its real call topology.**
92
+
93
+ - The in-memory harness mounts the bridge by hand-building a plugin object: `ctx.plugin({ name, inject, apply })`. That supplies `inject` manually, so it can never reproduce Bug #1 — `unwrapExports` is called only by the *Loader*, never by `ctx.plugin`. Even `ctx.plugin(NamespaceImport)` would not have caught it.
94
+ - The same harness mounts everything flat on one root context, so an `AgentLoop` resume reached from it either runs top-level (the `!runtime` bypass) or through a shadow whose origin still resolves on root — masking Bug #2's ancestor-walk failure.
95
+ - The only no-key e2e sent `initialize` and checked stdout purity. `initialize` never reaches the factory, so it sailed past both bugs.
96
+ - The only test that drove `session/new`/`session/load` was key-gated, so CI (no key) skipped it — and locally it "passed" only because a stale built `lib/` (with the old code) happened to satisfy module resolution.
97
+
98
+ 100% line coverage was satisfied the whole time. Coverage proves lines *ran*; it says nothing about whether the feature works *the way it ships*.
99
+
100
+ ## Guardrails added
101
+
102
+ - **Removed `export default apply`** (`packages/acp/acp/src/index.ts`) — the Bug #1 fix.
103
+ - **`AgentLoop.resume` reads `this.ctx.get('sessionPersistence')`** (`packages/core/agent-loop/src/index.ts`) — the Bug #2 fix, with a comment explaining the shadow-walk trap.
104
+ - **No-key `session/new` e2e over real stdio** (`examples/acp-agent/tests/acp.e2e.ts`): boots the example as a subprocess through the real Loader and asserts `session/new` resolves. This fails loudly on Bug #1 with no API key. Verified it fails when `export default apply` is restored.
105
+ - **`TSX_TSCONFIG_PATH` in the e2e spawn**: the subprocess runs from a temp cwd, where tsx cannot find the repo-root tsconfig `paths` map by searching upward — so dsh-* imports silently fell back to built `lib/`. Pointing tsx at the repo tsconfig makes resolution cwd-independent and ensures the test runs *source*, not a possibly-stale build.
106
+ - **[docs/testing.md](../testing.md) rule**: "test the real entry path", line coverage is not behavior coverage — codifies the lesson for every future plugin.
107
+
108
+ ## Lessons
109
+
110
+ - A namespace plugin and a default export are mutually exclusive under the cordis Loader. Pick the namespace form (`name`/`inject`/`Config`/`apply`) and do not add `export default` — `unwrapExports` will discard the namespace.
111
+ - For a service a plugin reads opportunistically but does NOT declare in `static inject`, use `ctx.get(name)`, never `ctx.<name>`. The property proxy resolves by an ancestor-only fiber walk that fails through a foreign shadow; `ctx.get(name)` is the topology-independent lookup (and strict by default — an inactive backend reads as `undefined` rather than being handed back mid-teardown).
112
+ - A test that constructs a plugin by hand cannot validate how the plugin loads. At least one test must drive the real Loader/export path end-to-end. When the headline operation does not call the model, that test needs no API key — so it belongs in CI, not behind a key gate.
113
+ - Trust the trace, not the theory. The elegant shadow explanation was real but was the *second* bug; the *first* was a one-line export mistake that a fiber-walk `console.error` found in minutes after hours of plausible-but-wrong reasoning.
@@ -0,0 +1,113 @@
1
+ # 事故复盘(postmortem)0001:ACP(Agent Client Protocol)服务器在连接时崩溃——`export default` 丢弃了插件的 `inject`
2
+
3
+ [English](0001-acp-default-export-drops-inject.md) | 中文
4
+
5
+ 状态:已解决;修复见 PR(Pull Request)#41 `feat/acp-2-bridge`
6
+
7
+ ## 摘要
8
+
9
+ 两个集成错误在单元测试全覆盖的情况下仍然导致 ACP 崩溃:一个默认导出使 Loader 丢弃了 `inject`,一个经可追踪代理的可选服务查找在 shadow 边界上失败。手动挂载的测试绕过了这两条路径。修复方案增加了无需 API key 的真实 Loader 测试覆盖,并为插件导出和可选服务访问制定了包级规则。
10
+
11
+ ## 概述
12
+
13
+ ACP 服务器(`examples/acp-agent`、`@deepseek-ai/dsh-acp`)在真实编辑器(Zed)连接的瞬间崩溃:第一个 `session/new` 请求返回 `Internal error: cannot get property "agents" without inject`,`session/load` 对 `sessionPersistence` 返回同样的错误。尽管有 178 个绿色单元测试和 100% 行覆盖率,bridge 在生产环境中完全无法工作。两个独立的 bug 隐藏在同一个错误字符串背后,测试套件之所以两个都没捕获,原因也相同:所有测试都通过一条不会触及插件真实加载方式和服务真实解析方式的路径来挂载插件。
14
+
15
+ ## 影响
16
+
17
+ ACP 服务器无法创建或加载任何一个会话——而这正是编辑器最先调用的两个 RPC。任何将 agent(智能体)接入 Zed 的人都会立即遭遇硬性失败。无数据丢失(崩溃前没有任何内容被持久化);代价完全是「功能不可用」加上两次定位原因的调试时间。
18
+
19
+ ## 时间线
20
+
21
+ - bridge(RFC 010)落地时有一套完整的单元测试,覆盖 codec、内存传输、生成的协议消息、失败路径和 HMR(热模块替换);另有一个需要 key 的真实 API e2e 测试和一个无需 key 的 stdout 纯净性 e2e 测试。全部绿色,100% 覆盖率。
22
+ - 真实 Zed 会话在 `session/new` 上立即失败,报错 `cannot get property "agents" without inject`。
23
+ - 调查最初沿着一个 Cordis「traceable/shadow」理论展开(看似合理,且该机制确实存在——见 Bug #2),随后在 vendor 目录中的 `reflect.ts` 里对实际 fiber 遍历做了插桩,并运行了真实子进程。跟踪结果显示,异常在 `apply()` 第 179 行、*插件加载时*抛出,位于 ROOT fiber 且没有 shadow——推翻了 shadow 理论对 `session/new` 的解释。
24
+ - 找到根因 #1:一行多余的 `export default apply`。删除后 `session/new` 修复。
25
+ - 删除后暴露了 Bug #2:`session/load` 仍然在 `sessionPersistence` 上抛错——这是一个真正不同的机制(shadow 遍历),通过隔离修复并重新运行真实子进程得到确认。
26
+
27
+ ## 根因 #1——`export default apply` 丢弃了插件的 `inject`(导致 `session/new` 崩溃)
28
+
29
+ `packages/acp/acp/src/index.ts` 是一个*命名空间插件*:它将 `name`、`inject`、`Config` 和 `apply` 作为独立的命名导出,仓库中其他所有插件(`invariants`、`llm-deepseek`、`tool-bash`、`tui` 等)也是如此。但它*还*多了一行其他插件都没有的代码:
30
+
31
+ ```ts ignore-check
32
+ export const name = 'acp'
33
+ export const inject = ['agents', 'sessions', 'sessionPersistence']
34
+ export function apply(ctx: Context, config: AcpConfig): void { /* … */ }
35
+ // …
36
+ export default apply // ← the bug
37
+ ```
38
+
39
+ 当插件从 `cordis.yml` 加载时,Cordis Loader 通过 `Loader.unwrapExports`(`vendor/loader/src/index.ts`)对导入的模块进行规范化:
40
+
41
+ ```ts ignore-check
42
+ unwrapExports(exports: any) {
43
+ if (isNullable(exports)) return exports
44
+ exports = exports.default ?? exports // ← prefers `.default`
45
+ if (!exports.__esModule) return exports
46
+ return exports.default ?? exports
47
+ }
48
+ ```
49
+
50
+ 存在默认导出时,`exports.default ?? exports` 解析为**裸 `apply` 函数**。裸函数没有 `inject`、没有 `name`、没有 `Config` 属性——这些作为*同级*命名导出存在于模块命名空间上,而 unwrap 到 `.default` 把整个命名空间丢弃了。Loader 随后基于空的 `inject` 构建了插件的 fiber。
51
+
52
+ 因此 `apply` 在一个**没有注入任何服务**的 fiber 中运行。第一行 `const agents = ctx.agents` 遍历 fiber 树(ROOT → Include → Loader → ROOT),在所有 fiber 的 store 中都找不到 `agents`,到达根 fiber(`runtime === null`)后抛出 `cannot get property "agents" without inject`。崩溃发生在*加载时*,而非后续的请求处理器中——请求只是恰好触发了加载。
53
+
54
+ **修复:**删除 `export default apply`。Loader 随后使用模块命名空间,正确识别 `inject`/`name`/`Config`,`apply` 在一个确实注入了所声明服务的 fiber 中运行。
55
+
56
+ ## 根因 #2——可选服务读取通过可追踪 shadow 触发 inject 守卫(导致 `session/load` 崩溃)
57
+
58
+ 修复 #1 后,`session/new` 正常工作,但 `session/load` 仍然抛出 `cannot get property "sessionPersistence" without inject`。这个问题*确实*源于 Cordis 的可追踪代理/shadow 机制,值得精确理解。
59
+
60
+ `session/load` 调用 `agents.resume(...)`,后者委托给 `AgentLoop.resume()`,其中读取了 `this.ctx.sessionPersistence`。`AgentLoop` 的 `static inject` 故意不包含 `sessionPersistence`——注入它会导致非持久化的演示永远挂起,等待一个永远不会加载的后端。该服务由一个独立的兄弟插件/fiber 提供,以机会性方式读取。
61
+
62
+ Cordis 中的服务访问通过上下文代理(`vendor/cordis/src/reflect.ts`)进行。当通过从另一条 fiber 获取的*可追踪代理*调用服务方法时(此处:bridge fiber 调用 `ctx.agents.resume`,注册表返回 `this.factory`——即 `AgentLoop`——重新包装为绑定到调用方的新 traceable 代理),`createShadowMethod`(`vendor/cordis/src/utils.ts`)将 `this` 重新绑定到一个 *shadow* 对象,其 `ctx` 携带 `[symbols.shadow]` 指向 `AgentLoop` 自身的构造上下文。在 `resume` 内部,`this.ctx.sessionPersistence` 的解析从 shadow 的 fiber 开始遍历:
63
+
64
+ ```ts ignore-check
65
+ // reflect.ts get handler
66
+ let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber // ← starts at AgentLoop's fiber
67
+ while (true) {
68
+ const impl = fiber.store?.[prop]
69
+ if (impl) return getTraceable(ctx, impl.value)
70
+ if (prop in fiber.inject) { /* inactive-context error */ }
71
+ if (!fiber.runtime) throw error // ← reached root, throw
72
+ if (fiber.parent[symbols.isolate][prop] !== key) throw error
73
+ fiber = fiber.parent.fiber // ← ancestor-only
74
+ }
75
+ ```
76
+
77
+ 遍历**仅向祖先方向**进行。`sessionPersistence` 既不在 `AgentLoop` 的 fiber store 中(不在其 `static inject` 中),也不在通往 root 的任何祖先上(它位于一个*兄弟*分支),因此遍历到达根 fiber 后抛错。
78
+
79
+ 为什么内存中的 `AgentLoop` 恢复测试没有捕获这个问题?因为它们从测试代码直接调用 `ctx.agents.resume(...)`——*在任何插件 fiber 之外*。此时 `ctx.fiber.runtime` 为 `null`,代理处理器走了一条提前绕过的路径:
80
+
81
+ ```ts ignore-check
82
+ if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false) // ← direct global-store lookup, no fiber walk
83
+ ```
84
+
85
+ `ctx.reflect.get(name, false)` 是基于 isolate symbol 的全局服务 store 直接查找——完全忽略 fiber 拓扑,能找到服务。因此从顶层测试读取可以成功;而从真实插件 fiber 内部、经由 shadow 到达时则抛错。bridge 恰好是后者。
86
+
87
+ **修复:**使用 `ctx.get('sessionPersistence')` 读取可选服务,该方法使用全局 isolate-keyed store 同时保留活跃状态检查。对于插件声明注入集中的服务,直接属性读取仍然适用。
88
+
89
+ ## 为什么所有测试都没有捕获(真正的失败)
90
+
91
+ 两个 bug 都源于同一个根本流程缺口:**没有任何测试通过插件的真实加载路径或真实调用拓扑来驱动它。**
92
+
93
+ - 内存 harness 通过手动构建插件对象来挂载 bridge:`ctx.plugin({ name, inject, apply })`。这手动提供了 `inject`,因此永远无法复现 Bug #1——`unwrapExports` 只被 *Loader* 调用,`ctx.plugin` 从不调用它。即使 `ctx.plugin(NamespaceImport)` 也无法捕获。
94
+ - 同一个 harness 将所有内容平铺挂载在一个根上下文上,因此从中触达的 `AgentLoop` 恢复要么运行在顶层(`!runtime` 绕过),要么经由 shadow 运行,而该 shadow 的 origin 仍然解析到 root——掩盖了 Bug #2 的祖先遍历失败。
95
+ - 唯一的无 key e2e 发送 `initialize` 并检查 stdout 纯净性。`initialize` 从不触达 factory,因此两个 bug 都安然通过。
96
+ - 唯一驱动 `session/new`/`session/load` 的测试需要 key 才能运行,因此 CI(无 key)跳过了它——而本地它之所以「通过」,只是因为一个陈旧的已构建 `lib/`(包含旧代码)恰好满足了模块解析。
97
+
98
+ 100% 行覆盖率始终满足。覆盖率证明代码行*被执行过*;它不能说明功能是否*按交付方式正常工作*。
99
+
100
+ ## 新增的防护措施
101
+
102
+ - **删除 `export default apply`**(`packages/acp/acp/src/index.ts`)——Bug #1 的修复。
103
+ - **`AgentLoop.resume` 使用 `this.ctx.get('sessionPersistence')`**(`packages/core/agent-loop/src/index.ts`)——Bug #2 的修复,附注释说明 shadow 遍历陷阱。
104
+ - **无需 key 的 `session/new` e2e,通过真实 stdio 运行**(`examples/acp-agent/tests/acp.e2e.ts`):以子进程方式通过真实 Loader 启动示例,并断言 `session/new` 正常返回。无需 API key 即可明确暴露 Bug #1。已验证恢复 `export default apply` 时测试失败。
105
+ - **e2e spawn 中设置 `TSX_TSCONFIG_PATH`**:子进程从临时 cwd 运行,tsx 无法通过向上搜索找到仓库根的 tsconfig `paths` 映射——因此 dsh-* 的 import 静默回退到已构建的 `lib/`。将 tsx 指向仓库 tsconfig 使解析不依赖 cwd,确保测试运行的是*源码*而非可能陈旧的构建产物。
106
+ - **[docs/testing.md](../testing.md) 规则**:「测试真实入口路径」,行覆盖率不等于行为覆盖率——将这一教训编纂为所有未来插件的规则。
107
+
108
+ ## 经验教训
109
+
110
+ - 命名空间插件与 default export 在 Cordis Loader 下互斥。选择命名空间形式(`name`/`inject`/`Config`/`apply`),不要添加 `export default`——`unwrapExports` 会丢弃命名空间。
111
+ - 对于插件机会性读取但未在 `static inject` 中声明的服务,使用 `ctx.get(name)`,绝不使用 `ctx.<name>`。属性代理通过仅向祖先方向的 fiber 遍历解析,经由外部 shadow 时会失败;`ctx.get(name)` 是拓扑无关的查找(且默认采用严格模式——非活跃后端读取为 `undefined`,不会在 teardown 期间仍将该后端返回给调用方)。
112
+ - 手动构建插件的测试无法验证插件的加载方式。至少一个测试必须端到端地驱动真实的 Loader/export 路径。当核心操作不调用模型时,该测试无需 API key——因此它属于 CI,而非 key 门控之后。
113
+ - 相信跟踪结果,不要迷信理论。优雅的 shadow 解释是真实的,但它是*第二个* bug;*第一个*是一行导出错误,在数小时看似合理但实际错误的推理之后,一个 fiber 遍历的 `console.error` 在几分钟内就找到了它。
@@ -0,0 +1,47 @@
1
+ # Post-mortem 0002: Filesystem snapshot tools were permanently disabled
2
+
3
+ English | [中文](0002-js-expression-disabled-filesystem-tools.zh.md)
4
+
5
+ Status: resolved
6
+
7
+ ## Executive summary
8
+
9
+ The ACP example attempted to enable filesystem plugins conditionally with `disabled: !!js ...`, but Cordis evaluates JavaScript expressions only inside plugin `config`. The raw expression object was truthy, so the filesystem stack was always disabled. Snapshot refresh then accepted `UNKNOWN_TOOL` results as new expected outputs. The fix uses an explicit filesystem overlay and adds static-config and snapshot-result guards.
10
+
11
+ ## Summary
12
+
13
+ The default ACP composition is intentionally bash-only because its sandbox cannot confine in-process filesystem providers. Filesystem snapshot scenarios still need `read`, `write`, and `edit`, so their plugins were placed in the default `cordis.yml` with a `disabled` expression intended to enable them only for full-access launches and snapshots.
14
+
15
+ Cordis Include parsed each `!!js` scalar into an expression object. The Loader recursively interpolated the plugin's `config`, but consumed entry metadata such as `disabled` directly. Every filesystem entry therefore saw a truthy object and remained disabled in every mode.
16
+
17
+ ## Impact
18
+
19
+ Seven filesystem scenarios and the mixed workspace-edit scenario called tools that were absent from the registry. Their structured session logs carried `ToolNotFoundError` with code `UNKNOWN_TOOL`, while stdout rendered generic failed tool cards. The snapshot suite passed because both outputs matched the refreshed fixtures; it proved deterministic replay of the regression rather than successful filesystem behavior.
20
+
21
+ The live confined default did not gain unintended filesystem access. A naive interpolation fix would have created that risk: permission presets update bash sandbox and approval state at runtime, but cannot mount, unmount, or confine the filesystem stack.
22
+
23
+ ## Timeline
24
+
25
+ - PR #261 consolidated ACP compositions and refreshed the filesystem snapshots while introducing conditional filesystem entries.
26
+ - All unit, coverage, snapshot, documentation, build, and hygiene checks passed.
27
+ - Review of the refreshed filesystem expected outputs found generic failed cards and structured `UNKNOWN_TOOL` results.
28
+ - A real Loader boot confirmed that every `disabled` value remained an expression object and every filesystem fiber was absent.
29
+
30
+ ## Root cause
31
+
32
+ The implementation assumed `!!js` applied to an entire Loader entry. It applies only to `entry.options.config`: `Entry._resolveConfig()` interpolates that field, while `Entry.disabled` tests `entry.options.disabled` without interpolation. The YAML tag was syntactically valid, so loading produced no diagnostic.
33
+
34
+ The snapshot framework treated any deterministic transcript as valid behavior. Header pins verified the composed tool schemas, but the filesystem scenarios shared a pin from the default composition and therefore did not independently prove that their required tools were registered. Refresh rewrote the expected stdout and session logs before any semantic assertion rejected missing tools.
35
+
36
+ ## Guardrails added
37
+
38
+ - Filesystem scenarios boot `fs.cordis.yml`, an explicit fixed full-access overlay with a paired replay config and its own request-header class.
39
+ - [`AGENTS.md`](../../AGENTS.md) and the [Cordis primer](../cordis-primer.md#loader-configuration) state that `!!js` is valid only under plugin `config` and conditional composition uses overlays.
40
+ - `verify-cordis-config` parses repository Cordis YAML and rejects expression nodes in Loader entry metadata, including include patches and inserted entries.
41
+ - `dsh-acp-snapshot` rejects structured `UNKNOWN_TOOL` results in fresh runs and committed session fixtures before they can be committed as expected outputs.
42
+
43
+ ## Lessons
44
+
45
+ - A syntactically accepted configuration value is not necessarily evaluated at that location; document and verify exactly which fields are interpolated.
46
+ - A snapshot refresh is fixture production, not correctness review. Semantic impossibilities such as a missing registered tool need assertions independent of the expected output.
47
+ - Permission controls must describe only the capabilities they actually govern. Composition-time filesystem access cannot follow a runtime bash-only preset safely.
@@ -0,0 +1,47 @@
1
+ # 事故复盘(postmortem) 0002:文件系统快照工具被永久禁用
2
+
3
+ [English](0002-js-expression-disabled-filesystem-tools.md) | 中文
4
+
5
+ 状态:已解决
6
+
7
+ ## 摘要
8
+
9
+ ACP(Agent Client Protocol)示例试图通过 `disabled: !!js ...` 有条件地启用文件系统插件,但 Cordis 仅在插件 `config` 内部对 JavaScript 表达式求值。原始的表达式对象为 truthy,因此文件系统栈始终处于禁用状态。快照刷新随后将 `UNKNOWN_TOOL` 结果接受为新的预期输出。修复方案改用显式的文件系统 overlay,并增加了静态配置守卫和快照结果守卫。
10
+
11
+ ## 概述
12
+
13
+ 默认的 ACP 组合有意只启用 bash,因为其沙箱无法约束进程内的文件系统提供方。文件系统快照场景仍然需要 `read`、`write` 和 `edit`,因此这些插件被放在默认的 `cordis.yml` 中,并附带一个 `disabled` 表达式,意图仅在全权限启动和快照模式下启用它们。
14
+
15
+ Cordis Include 将每个 `!!js` 标量解析为一个表达式对象。Loader 递归地对插件的 `config` 进行插值,但直接读取 `disabled` 等配置项元数据。因此每个文件系统配置项看到的都是一个 truthy 对象,在所有模式下均保持禁用。
16
+
17
+ ## 影响
18
+
19
+ 七个文件系统场景和一个混合工作区编辑场景调用了注册表中不存在的工具。其结构化会话日志携带 `ToolNotFoundError`(code 为 `UNKNOWN_TOOL`),stdout 渲染出通用的失败工具卡片。快照套件通过了,因为结构化会话日志和 stdout 渲染出的通用失败工具卡片均与刷新后的 fixture(测试前置数据)匹配;它证明的是回归的确定性回放,而非文件系统行为的正确性。
20
+
21
+ 实际运行的受限默认模式并未获得意外的文件系统访问权限。草率地直接修复插值反而会带来这一风险:权限预设在运行时更新 bash 沙箱和审批状态,但无法挂载、卸载或约束文件系统栈。
22
+
23
+ ## 时间线
24
+
25
+ - PR(Pull Request) #261 整合了 ACP 组合并刷新了文件系统快照,同时引入了条件式文件系统配置项。
26
+ - 所有单元测试、覆盖率、快照、文档、构建和 hygiene 检查均通过。
27
+ - 对刷新后的文件系统预期输出的评审发现了通用的失败卡片和结构化的 `UNKNOWN_TOOL` 结果。
28
+ - 一次真实的 Loader 启动确认:每个 `disabled` 值仍为表达式对象,每个文件系统 fiber 均未创建。
29
+
30
+ ## 根因
31
+
32
+ 实现时假设 `!!js` 适用于整个 Loader 配置项。实际只有 `entry.options.config` 使用它:`Entry._resolveConfig()` 对该字段进行插值,而 `Entry.disabled` 直接测试 `entry.options.disabled`,不经过插值。YAML 标签在语法上合法,因此加载过程不产生任何诊断信息。
33
+
34
+ 快照框架将任何确定性的 transcript(文本记录)视为有效行为。Header pin 验证了组合后的工具 schema,但文件系统场景共享来自默认组合的 pin,因此未独立证明其所需工具已注册。刷新在任何语义断言拒绝缺失工具之前,就已重写了预期的 stdout 和会话日志。
35
+
36
+ ## 已添加的防护措施
37
+
38
+ - 文件系统场景启动 `fs.cordis.yml`:一个显式的固定全权限 overlay,配有对应的回放配置和独立的 request-header 类。
39
+ - [`AGENTS.md`](../../AGENTS.md) 与 [Cordis 入门](../cordis-primer.md#loader-configuration)明确说明 `!!js` 仅在插件 `config` 内有效,条件式组合应使用 overlay。
40
+ - `verify-cordis-config` 解析仓库中的 Cordis YAML,拒绝 Loader 配置项元数据中的表达式节点(包括 include patch 和插入的配置项)。
41
+ - `dsh-acp-snapshot` 在全新运行和已提交的会话 fixture 中拒绝结构化的 `UNKNOWN_TOOL` 结果,防止其被提交为预期输出。
42
+
43
+ ## 教训
44
+
45
+ - 语法上被接受的配置值不一定在该位置被求值;应记录并验证具体对哪些字段进行插值。
46
+ - 快照刷新是 fixture 的生产过程,不是正确性审查。诸如已注册工具缺失这类语义上不可能的结果,需要独立于预期输出的断言。
47
+ - 权限控制只应描述其实际管辖的能力。组合时的文件系统访问无法安全地跟随运行时的 bash-only 预设。
@@ -0,0 +1,53 @@
1
+ # Post-mortem 0003: Web agent validated a replacement server instead of its current GUI
2
+
3
+ English | [中文](0003-web-agent-gui-feedback-loop.zh.md)
4
+
5
+ Status: resolved
6
+
7
+ ## Executive summary
8
+
9
+ A Web agent changed the GUI source but did not know which URL and process hosted its session. It delegated acceptance to the user, then treated a bare Vite HTTP 200 as success despite a missing `window.__DSH_BOOT__` white screen, and finally validated a replacement `dsh web` server on another port while the original page had already picked up rebuilt artifacts. The fix makes the current URL and runtime mode model-visible and shell-queryable, rejects standalone Vite before listen, and verifies production refresh and development HMR against external state.
10
+
11
+ ## Summary
12
+
13
+ The session ran inside the DeepSeek Harness Web GUI at port 3081 while its selected Workspace was an empty `test/` directory. The model request named neither the GUI nor its source checkout, URL, process, or update mode. Repository affordances exposed `apps/web` with a Vite development script, while the full browser composition lived behind `dsh web`.
14
+
15
+ The resulting actions were individually plausible but did not share one acceptance target. A source edit, a successful build, an HTTP 200, an injected boot manifest, and the user's existing page were treated as interchangeable facts.
16
+
17
+ The evidence source is the persisted event log for `session-3eb796c2-5159-4686-affe-df8719f6f987`, whose header records cwd `/Users/tn.shen/Documents/deepseek-harness-gui-master/test`. Its initial request header is sequence 6; the user-facing handoff, bare-Vite launch, replacement-host launch, boot-manifest probe, and first 3081 process probe are sequences 30939, 31865, 34309, 34441, and 34681 respectively. The timeline below follows those events rather than reconstructing intent from the later report.
18
+
19
+ ## Impact
20
+
21
+ The user had to identify three consecutive mistakes: acceptance was delegated back to them; the proposed preview was a blank page; and the reported successful URL was not the page they were using. An unmanaged replacement server also outlived the turn until the user challenged it.
22
+
23
+ No change in this investigation restarted or modified the read-only 3081 and 3082 trial services.
24
+
25
+ ## Timeline
26
+
27
+ - In turn 2, after editing the theme, the agent's sequence-30939 message told the user to run `pnpm run demo:tui` or open an unspecified Web application. It ran no assembled Web acceptance.
28
+ - In turn 3, the agent read `apps/web/package.json`, launched bare Vite on port 5173 at sequence 31865, observed HTTP 200, and declared success. The browser instead threw `client-modules: window.__DSH_BOOT__ is missing or not an object` and rendered a white page.
29
+ - In turn 4, the agent found the full `dsh web` path, rebuilt the shell, launched an unmanaged process on port 3334 at sequence 34309, and checked only that this replacement returned 200 with a boot manifest at sequence 34441. It never probed port 3081.
30
+ - In turn 5, the user reported at sequence 34556 that 3081 already showed the new theme. Only then, at sequence 34681, did the agent inspect the existing process and remove the redundant server.
31
+
32
+ ## Root cause
33
+
34
+ The Web assembly had no model-visible identity for the current GUI, canonical URL, or runtime mode. The session cwd correctly identified the user's selected Workspace, but the model treated that project directory as the application directory. No durable record related the GUI source checkout, built artifacts, serving process, target origin, and browser acceptance.
35
+
36
+ The wrong startup path looked legitimate because bare Vite returned HTTP 200. `window.__DSH_BOOT__` is injected only by the full host, so transport readiness did not imply application readiness. The first regression test repeated this mistake in another form: a timeout killed Vite and satisfied a nonzero-exit assertion. Live reproduction exposed that false positive.
37
+
38
+ Background process semantics were also bypassed with shell `&`, so job identity, completion notices, collection, and cleanup did not apply. Verifying port 3334 therefore proved only that a second service worked.
39
+
40
+ ## Guardrails added
41
+
42
+ - The Web launcher publishes the canonical loopback URL and actual production/development mode in the logged `app:web-surface` prompt section and managed `$DSH_WEB_URL`/`$DSH_WEB_MODE` environment.
43
+ - Production guidance requires rebuilding artifacts and verifying the existing URL after refresh. Development guidance explains that `dsh web --dev` mounts only the HMR receiver; `pnpm run dev:web` in the same checkout must also rebuild client-plugin bundles, while shell and plain-package changes still require refresh.
44
+ - `apps/web` standalone Vite serve mode rejects during configuration. Its subprocess test proves natural exit and instruments `Server.listen()` so a transient bind cannot pass unnoticed.
45
+ - Layered real-path tests cover the CLI request, exact production/development prompts, shell runtime facts, same-port static replacement, source watcher rebuild, host stat polling, and browser HMR under an unchanged page identity.
46
+ - PR evidence preserves screenshots from the original 3081 session and a real-model before/after GUI run; external browser, HTTP, process, and session-log observations carry acceptance.
47
+
48
+ ## Lessons
49
+
50
+ - The agent must know hidden runtime prerequisites before it can guide the user; startup mode is application context, not tribal knowledge.
51
+ - HTTP readiness, build success, and a boot manifest are different facts. Acceptance names the exact origin and externally observes the requested change there.
52
+ - A replacement service cannot prove that an existing page changed. Long-running processes use managed task lifecycles when they are actually requested.
53
+ - A regression test must be able to fail for the reported mechanism. Process timeout is not equivalent to fail-fast, and post-exit port availability does not prove the port was never bound.
@@ -0,0 +1,53 @@
1
+ # 事故复盘(postmortem) 0003:Web agent(智能体)验收了替代服务器,而非其当前 GUI
2
+
3
+ [English](0003-web-agent-gui-feedback-loop.md) | 中文
4
+
5
+ 状态:已解决
6
+
7
+ ## 摘要
8
+
9
+ Web agent 修改了 GUI 源码,却不知道当前会话对应哪个 URL、由哪个进程承载。它把验收交还给用户,随后在 `window.__DSH_BOOT__` 缺失导致白屏的情况下,仍把裸 Vite 返回的 HTTP 200 当作成功;最后,原页面其实已经加载了重建产物,它却去验收另一个端口上的替代 `dsh web` 服务器。修复让当前 URL 和运行模式对模型可见且可由 shell 查询,在独立 Vite 开始监听前拒绝启动,并依据外部状态验收生产模式刷新与开发模式 HMR(热模块替换)。
10
+
11
+ ## 概述
12
+
13
+ 该会话运行在端口 3081 的 DeepSeek Harness Web GUI 中,而用户选择的 Workspace 是空的 `test/` 目录。模型请求既未指明该 GUI,也未提供它的源码检出目录、URL、进程或更新模式。仓库在 `apps/web` 中提供了 Vite 开发脚本,完整的浏览器组合则由 `dsh web` 提供。
14
+
15
+ 由此产生的各个动作单看都合理,却没有指向同一个验收目标。源码修改、成功构建、HTTP 200、注入的启动 manifest(元数据清单)和用户原本打开的页面,被当成了可以相互替代的事实。
16
+
17
+ 证据源是 `session-3eb796c2-5159-4686-affe-df8719f6f987` 的持久化事件日志,其头部记录的 cwd 为 `/Users/tn.shen/Documents/deepseek-harness-gui-master/test`。初始请求头位于序列 6;面向用户的交接、裸 Vite 启动、替代宿主启动、启动 manifest 探测,以及首次探测 3081 进程,分别位于序列 30939、31865、34309、34441 和 34681。下方时间线以这些事件为依据,而不是根据后续报告反推意图。
18
+
19
+ ## 影响
20
+
21
+ 用户不得不连续指出三个错误:agent 把验收交还给用户;建议预览的页面一片空白;报告成功的 URL 并不是用户正在使用的页面。一个不受管理的替代服务器还持续运行到下一个轮次,直到用户提出质疑。
22
+
23
+ 本次调查没有重启或修改只读的 3081 和 3082 试验服务。
24
+
25
+ ## 时间线
26
+
27
+ - 在第 2 个轮次中,agent 修改主题后,在序列 30939 的消息中让用户运行 `pnpm run demo:tui` 或打开一个未明确指定的 Web 应用。它没有对组装后的 Web 应用执行任何验收。
28
+ - 在第 3 个轮次中,agent 读取 `apps/web/package.json`,在序列 31865 于端口 5173 上启动裸 Vite,观察到 HTTP 200 后便宣布成功。浏览器却抛出 `client-modules: window.__DSH_BOOT__ is missing or not an object`,并显示白屏。
29
+ - 在第 4 个轮次中,agent 找到了完整的 `dsh web` 启动路径,重新构建 shell,在序列 34309 于端口 3334 上启动一个不受管理的进程,并且只在序列 34441 检查了这个替代服务是否返回 200 和启动 manifest。它从未探测端口 3081。
30
+ - 在第 5 个轮次中,用户在序列 34556 报告 3081 已经显示新主题。直到序列 34681,agent 才检查既有进程并移除冗余服务器。
31
+
32
+ ## 根因
33
+
34
+ Web 组合没有向模型提供当前 GUI、规范 URL 或运行模式的身份信息。会话 cwd 正确标识了用户选择的 Workspace,但模型把这个项目目录当成了应用目录。系统也没有持久记录将 GUI 源码检出目录、构建产物、服务进程、目标 origin 和浏览器验收关联起来。
35
+
36
+ 裸 Vite 返回 HTTP 200,使错误的启动路径看似合理。`window.__DSH_BOOT__` 只由完整宿主注入,因此传输层就绪不代表应用已就绪。首个回归测试以另一种方式重复了同样的错误:超时机制终止 Vite 后,非零退出断言仍会通过。真实复现暴露了这一误报。
37
+
38
+ agent 还通过 shell `&` 绕过了后台进程语义,因此任务身份、完成通知、结果收集和清理机制均未生效。验证端口 3334 只能证明第二个服务可以工作。
39
+
40
+ ## 已添加的防护措施
41
+
42
+ - Web 启动器在记录到日志的 `app:web-surface` 提示词区段,以及受管的 `$DSH_WEB_URL`/`$DSH_WEB_MODE` 环境变量中,发布规范环回 URL 和实际的生产/开发模式。
43
+ - 生产模式指南要求重新构建产物,并在刷新后验证既有 URL。开发模式指南说明,`dsh web --dev` 只挂载 HMR 接收端;同一源码检出目录中的 `pnpm run dev:web` 还必须重新构建客户端插件 bundle,而 Web shell 和普通包的改动仍然需要刷新页面。
44
+ - `apps/web` 的独立 Vite 服务模式会在配置阶段拒绝启动。其子进程测试验证进程自然退出,并插桩 `Server.listen()`,确保短暂绑定端口也不会漏检。
45
+ - 分层的真实路径测试覆盖 CLI(命令行界面)请求、精确的生产/开发模式提示词、shell 运行时事实、同端口静态产物替换、源码 watcher 重建、宿主 stat 轮询,以及页面 identity 不变的浏览器 HMR。
46
+ - PR(Pull Request)证据保留了原始 3081 会话的截图,以及真实模型驱动的 GUI 修改前后对比;验收以外部浏览器、HTTP、进程和会话日志的观测结果为准。
47
+
48
+ ## 教训
49
+
50
+ - agent 必须先知道隐藏的运行时前置条件,才能指导用户;启动模式属于应用上下文,不应依赖团队口口相传。
51
+ - HTTP 就绪、构建成功和启动 manifest 是不同的事实。验收必须明确指定确切的 origin,并从外部观察所请求的改动是否在该 origin 生效。
52
+ - 替代服务无法证明既有页面已经改变。确实收到启动长时间运行进程的请求时,应使用受管任务生命周期。
53
+ - 回归测试必须能够针对所报告的机制失败。进程超时不等同于快速失败,进程退出后端口可用也不能证明该端口从未被绑定。
@@ -0,0 +1,55 @@
1
+ # Post-mortem 0004: Landlock partial-enforcement notice misclassified child failures
2
+
3
+ English | [中文](0004-landlock-partial-notice-misclassified-child-failures.zh.md)
4
+
5
+ Status: resolved
6
+
7
+ ## Executive summary
8
+
9
+ 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`; the then-bash-backed filesystem search also 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, and an assembled keyless scenario pins the surviving bash path. Filesystem search uses packaged ripgrep through the subprocess seam and does not cross sandboxed bash.
10
+
11
+ ## Summary
12
+
13
+ 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.
14
+
15
+ 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.
16
+
17
+ At the time of the incident, filesystem search added a second attribution error. Its bash-backed `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.
18
+
19
+ ## Impact
20
+
21
+ 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.
22
+
23
+ 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.
24
+
25
+ ## Timeline
26
+
27
+ - 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.
28
+ - 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.
29
+ - 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.
30
+ - A minimal POSIX wrapper that prints the notice and `exec`s its payload reproduced the failure with `false` and ripgrep no-match.
31
+ - Structured rules plus shared foreground/background classification and assembled replay coverage closed the surviving sandbox attribution gap. Filesystem search uses packaged ripgrep through `ctx.subprocess`; the fix leaves that path outside sandboxed bash.
32
+
33
+ ## Root cause
34
+
35
+ 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.
36
+
37
+ 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. In the incident-era search implementation, filesystem-search tests modeled raw spawn errors but not a structured error thrown by the real sandboxed bash composition.
38
+
39
+ 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.
40
+
41
+ ## Guardrails added
42
+
43
+ - [`RunnerFailureRule`](../subsystems/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.
44
+ - [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) maps Landlock to exit 125 plus a non-notice `landlock-run:` line while bwrap, Seatbelt, and custom runners remain signature-only.
45
+ - [`dsh-bash-sandbox`](../../packages/shell/bash-sandbox/) directly spawns the provider argv, so a pre-start rejection uses the spawn-error channel instead of localized shell diagnostics. Settled foreground and background execution share one evidence-returning classifier; fatal evidence outranks denial, and foreground errors report the matched fatal line without changing captured stderr.
46
+ - [`dsh-tool-fs-search`](../../packages/fs/tool-fs-search/) uses packaged ripgrep through `ctx.subprocess` and remains outside the sandboxed bash seam.
47
+ - The native-boundary regression cases live in [`partial-landlock.spec.ts`](../../packages/shell/bash-sandbox/tests/partial-landlock.spec.ts), including informational notices, fatal evidence, and foreground/background classification.
48
+ - The assembled product path is pinned by the [`partial-landlock` snapshot composition](../../examples/acp-agent/partial-landlock.cordis.snapshot.yml), independently of filesystem-search implementation choices.
49
+
50
+ ## Lessons
51
+
52
+ - Process attribution requires a conjunction of independent evidence; a shared prefix is not a protocol.
53
+ - Informational and fatal diagnostics can share a namespace, so exclusions must be exact and narrow while unknown fatal lines stay fail-closed.
54
+ - An adapter must preserve structured failures owned by the seam below it instead of replacing them with its own nearest generic category.
55
+ - 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.
@@ -0,0 +1,55 @@
1
+ # 事故复盘(postmortem) 0004:Landlock 部分强制执行通知导致子进程失败被误归类
2
+
3
+ [English](0004-landlock-partial-notice-misclassified-child-failures.md) | 中文
4
+
5
+ Status: resolved
6
+
7
+ ## 摘要
8
+
9
+ 在 Landlock ABI 较旧的内核上,launcher 会在执行每个子进程前打印一条无害的部分强制执行通知。harness 把共享的 `landlock-run:` 前缀与任意非零子进程退出组合起来,判定为 launcher 失败,因此 ripgrep 在没有匹配项时以 1 退出等普通结果会呈现为 `SANDBOX_UNAVAILABLE`;当时仍由 bash 支撑的文件系统搜索还会用 `SEARCH_FAILED` 遮蔽这个结构化错误。过于宽泛的签名规则,以及缺少较旧 ABI 下部分强制执行的组合测试覆盖,让该缺陷得以流入。runner 分类现在会先精确排除信息性行,再要求由退出状态门控的致命证据,并由一个组装后的无密钥场景固定仍然存在的 bash 路径。文件系统搜索通过 subprocess seam 运行打包的 ripgrep,不经过沙箱化 bash。
10
+
11
+ ## 概述
12
+
13
+ 原生 launcher 约定区分两类 stderr 行。内核只能部分强制执行时,会精确打印 `landlock-run: partial enforcement (older Landlock ABI)`,然后继续执行子进程。launcher 失败则打印另一行 `landlock-run:` 诊断,在不执行子进程的情况下以 125 退出。
14
+
15
+ harness 用一个不区分大小写的 `landlock-run: ` 子串表示这两种情况。消费方只要发现非零退出同时携带该子串,就会归类为 runner 失败。因此,子进程的退出状态被错误地关联到 launcher 的信息性行:`false`、ripgrep 无匹配时的退出码 1、无效 pattern 的退出码 2,乃至由子进程自行选择的退出码 125,都可能在约束与执行均成功的情况下被错误归因为沙箱故障。
16
+
17
+ 事故发生时,文件系统搜索又造成第二处归因错误。当时由 bash 支撑的 `runRipgrep()` 会捕获 bash 执行器除中止外抛出的所有错误,并将其替换为关于 cwd 或 shell 启动的通用 `SEARCH_FAILED`,其中也包括沙箱执行器产生的结构化 `SandboxUnavailableError`。
18
+
19
+ ## 影响
20
+
21
+ 在 Landlock ABI 只能部分强制执行的主机上,合法的非零子进程结果可能表现为沙箱基础设施故障。`glob` 和 `grep` 尤其容易暴露该问题,因为 ripgrep 把退出码 1 用作成功的空搜索。当文件系统搜索中确实发生沙箱故障时,调用方也会丢失其 `SANDBOX_UNAVAILABLE` 错误码,转而收到错误的启动诊断。
22
+
23
+ 该缺陷没有削弱约束,也没有让命令在无约束状态下运行。其安全影响在于可用性与诊断完整性:有效的受限结果会被拒绝或错误标记。
24
+
25
+ ## 时间线
26
+
27
+ - 原生 launcher 约定规定:launcher 失败使用退出码 125,每次此类失败都会打印一行致命的 `landlock-run:` 诊断;成功执行子进程时则打印精确的部分强制执行通知。
28
+ - 沙箱提供方把该约定简化为 `runnerFailureSignatures: ['landlock-run: ']`;bash 消费方将此前缀与任意非零退出组合,并报告 stderr 的第一行。
29
+ - 单元测试覆盖了无诊断的成功、拒绝诊断和致命 runner 前缀。真实 runner 测试在没有可用内核时会自行跳过,也没有强制构造「部分强制执行通知后跟非零子进程退出」的情况。
30
+ - 一个最小 POSIX 包装脚本会打印该通知并 `exec` 其负载;它通过 `false` 与 ripgrep 无匹配场景复现了故障。
31
+ - 结构化规则、前台与后台共享的分类逻辑和组装后的回放覆盖共同弥补了仍然存在的沙箱归因缺口。文件系统搜索通过 `ctx.subprocess` 运行打包的 ripgrep;本修复让该路径继续位于沙箱化 bash 之外。
32
+
33
+ ## 根因
34
+
35
+ 公开的沙箱结果类型只能表达一组子字符串。它无法表示 Landlock 失败必须使用退出码 125、证据必须出现在一行致命诊断内,或同一前缀下有一行精确文本属于信息性通知。消费方的布尔判定逻辑因此把来自不同进程且互不相关的事实组合在一起;即便致命证据位于后续行,它仍选用 stderr 的第一行作为详细信息。
36
+
37
+ 测试矩阵与这种表示方式一致。模拟提供方要么不输出 runner 行,要么输出含义明确的致命前缀,从不在由子进程控制的非零退出前输出无害 runner 行。真实 Landlock 覆盖依赖主机 ABI,因此使用完整 ABI 的主机无法覆盖该通知。在事故发生时的搜索实现中,文件系统搜索测试模拟了原始 spawn 错误,却没有覆盖真实沙箱化 bash 组合抛出的结构化错误。
38
+
39
+ stderr 仍是带内归因通道。受限子进程可以故意复现 runner 的门控致命诊断行与退出状态,造成可用性或诊断误归因。更严格的多项证据合取可以避免本次事故中的意外冲突,但无法验证写入者身份;带外状态协议仍属于独立的加固工作,而非沙箱绕过修复。
40
+
41
+ ## 已添加的防护措施
42
+
43
+ - [`RunnerFailureRule`](../subsystems/sandbox.md#wrapped-argv-and-classification-dialects) 携带可选的允许退出码、不区分大小写的逐行致命签名,以及按不区分大小写的整行精确匹配排除的信息性行。
44
+ - [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) 把 Landlock 映射为退出码 125 加一行非通知的 `landlock-run:` 诊断,而 bwrap、Seatbelt 和自定义 runner 仍仅依据签名。
45
+ - [`dsh-bash-sandbox`](../../packages/shell/bash-sandbox/) 直接 spawn 提供方 argv,因此启动前遭拒时使用 spawn 错误通道,而非本地化的 shell 诊断。已结算的前台与后台执行共用一个返回证据的分类器;致命证据优先于拒绝,前台错误会报告匹配到的致命行,同时保持捕获的 stderr 不变。
46
+ - [`dsh-tool-fs-search`](../../packages/fs/tool-fs-search/) 通过 `ctx.subprocess` 运行打包的 ripgrep,并继续位于沙箱化 bash seam 之外。
47
+ - 原生边界回归用例位于 [`partial-landlock.spec.ts`](../../packages/shell/bash-sandbox/tests/partial-landlock.spec.ts),包括信息性通知、致命证据和前台/后台分类。
48
+ - 组装后的产品路径由 [`partial-landlock` 快照组合](../../examples/acp-agent/partial-landlock.cordis.snapshot.yml)固定,独立于文件系统搜索的实现选择。
49
+
50
+ ## 教训
51
+
52
+ - 进程归因需要多项独立证据同时成立;共享前缀不是协议。
53
+ - 信息性诊断与致命诊断可以共享同一命名空间,因此排除规则必须精确且范围狭窄,同时对未知的致命行保持失败关闭。
54
+ - 适配器必须保留下层 seam 所拥有的结构化失败,而不能用自身最接近的通用类别将其替换。
55
+ - 平台相关行为需要在原生边界放置确定性的模拟实现,并覆盖一条组装后的产品路径;会自行跳过的真实内核测试无法独自固定该回归。
@@ -0,0 +1,18 @@
1
+ # Post-mortems
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Incident write-ups: a bug reached a place it shouldn't have (a real user, a merged PR, a release), and the interesting part is *why our process let it through*, not just the one-line fix.
6
+
7
+ A post-mortem is NOT an [Agent Note](../../.agents/notes/README.md) (which records a deliberate design decision and its rejected alternatives, or proposes future work). It is a backward-looking record of a failure: what broke, the mechanism, why every safety net missed it, and the concrete guardrails added so the same class of bug fails loudly next time.
8
+
9
+ Write one when a bug is **subtle** (the mechanism is non-obvious and a careful engineer would re-derive it the hard way), **systemic** (the reason it escaped is a gap in tests/tooling/conventions, not a one-off typo), and **costly to rediscover** (it cost real debugging time, and would cost it again). Link the guardrails (tests, AGENTS.md rules, ADRs) the post-mortem motivated.
10
+
11
+ Every post-mortem opens with an **Executive summary**: one short paragraph a busy reader can absorb in thirty seconds — what broke, the root cause in plain terms, why it escaped, and the durable lesson — before the detailed Summary / Timeline / Root cause / Guardrails sections that follow.
12
+
13
+ | # | Title |
14
+ |---|---|
15
+ | [0001](0001-acp-default-export-drops-inject.md) | ACP server crashed on connect: `export default` dropped the plugin's `inject` |
16
+ | [0002](0002-js-expression-disabled-filesystem-tools.md) | Filesystem snapshot tools were permanently disabled by a literal `!!js` object |
17
+ | [0003](0003-web-agent-gui-feedback-loop.md) | Web agent validated a replacement server instead of the GUI hosting its session |
18
+ | [0004](0004-landlock-partial-notice-misclassified-child-failures.md) | Landlock partial-enforcement notice misclassified child failures |
@@ -0,0 +1,18 @@
1
+ # 事故复盘(postmortem)
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 事故复盘记录的是:一个 bug 出现在了不该出现的地方(真实用户、已合并的 PR(Pull Request)、已发布的版本),值得关注的是*为什么我们的流程放过了它*,而不仅仅是那一行修复。
6
+
7
+ 事故复盘不是 [Agent Note](../../.agents/notes/README.md)(Agent Note 记录一个经过深思熟虑的设计决策及其被否决的替代方案,或提出未来工作)。它是一份回顾性的失败记录:什么坏了、机制是什么、为什么每道安全网都没拦住、以及为此新增了哪些具体防护措施,以确保同类 bug 下次出现时会明确报错。
8
+
9
+ 当一个 bug 满足以下条件时,请撰写事故复盘:**隐蔽**(机制不显而易见,即使是细心的工程师也得费力重新推导)、**系统性**(逃逸的原因是测试、工具、约定的缺口,而非一次性的笔误)、**重新发现的代价高**(它消耗了真实的调试时间,且下次还会如此)。请链接该事故复盘所推动建立的防护措施(测试、AGENTS.md 规则、ADR)。
10
+
11
+ 每篇事故复盘以一段**执行摘要**开头:一个简短段落,让忙碌的读者在三十秒内吸收要点——什么坏了、用直白的话说根因是什么、为什么逃逸了、可长期沿用的教训是什么——然后才是后续的详细「概述、时间线、根因、防护措施」各节。
12
+
13
+ | # | 标题 |
14
+ |---|---|
15
+ | [0001](0001-acp-default-export-drops-inject.md) | ACP(Agent Client Protocol)服务器在连接时崩溃:`export default` 丢失了插件的 `inject` |
16
+ | [0002](0002-js-expression-disabled-filesystem-tools.md) | 文件系统快照工具被一个字面量 `!!js` 对象永久禁用 |
17
+ | [0003](0003-web-agent-gui-feedback-loop.md) | Web agent(智能体)验证了替代服务器,而非承载其会话的 GUI |
18
+ | [0004](0004-landlock-partial-notice-misclassified-child-failures.md) | Landlock 部分强制执行通知导致子进程失败被误归类 |