@v587d/capital-generation 2.8.2 → 2.9.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 (164) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +41 -20
  3. package/chart-ui/README.md +1 -1
  4. package/chart-ui/client.src.cjs +7 -16
  5. package/chart-ui/index.js +12 -33
  6. package/chart-ui/package.json +2 -2
  7. package/config/client.js +18 -39
  8. package/config/client.src.cjs +79 -217
  9. package/config/index.js +22 -72
  10. package/config/package.json +2 -2
  11. package/cordis.patch.yml +46 -51
  12. package/lib/agents/bash-guard.d.ts +9 -55
  13. package/lib/agents/bash-guard.js +27 -67
  14. package/lib/agents/preset-roster.d.ts +22 -0
  15. package/lib/agents/preset-roster.js +87 -0
  16. package/lib/agents/role-tool-policy.d.ts +46 -0
  17. package/lib/agents/role-tool-policy.js +205 -0
  18. package/lib/agents/root-tool-policy.d.ts +28 -124
  19. package/lib/agents/root-tool-policy.js +164 -237
  20. package/lib/agents/selected-skills-provider.d.ts +2 -6
  21. package/lib/agents/selected-skills-provider.js +27 -84
  22. package/lib/capital-settings.d.ts +27 -0
  23. package/lib/capital-settings.js +23 -0
  24. package/lib/chart/artifact-ref.d.ts +3 -8
  25. package/lib/chart/artifact-ref.js +3 -8
  26. package/lib/chart/downsample.d.ts +2 -5
  27. package/lib/chart/downsample.js +3 -8
  28. package/lib/chart/errors.d.ts +3 -7
  29. package/lib/chart/errors.js +1 -2
  30. package/lib/chart/event-context.d.ts +8 -0
  31. package/lib/chart/event-context.js +39 -0
  32. package/lib/chart/events.d.ts +21 -78
  33. package/lib/chart/events.js +40 -189
  34. package/lib/chart/html.d.ts +2 -5
  35. package/lib/chart/html.js +8 -15
  36. package/lib/chart/runtime.d.ts +5 -18
  37. package/lib/chart/runtime.js +14 -34
  38. package/lib/chart/series.d.ts +10 -17
  39. package/lib/chart/series.js +22 -47
  40. package/lib/chart/source-token.d.ts +5 -13
  41. package/lib/chart/source-token.js +18 -17
  42. package/lib/chart/spec.d.ts +6 -12
  43. package/lib/chart/spec.js +7 -31
  44. package/lib/chart/tool.d.ts +6 -29
  45. package/lib/chart/tool.js +9 -23
  46. package/lib/chart-render-entry.d.ts +9 -0
  47. package/lib/chart-render-entry.js +17 -0
  48. package/lib/chart-source-entry.d.ts +13 -0
  49. package/lib/chart-source-entry.js +20 -0
  50. package/lib/collect-entry.d.ts +9 -0
  51. package/lib/collect-entry.js +10 -0
  52. package/lib/collector-runtime.d.ts +22 -0
  53. package/lib/collector-runtime.js +97 -0
  54. package/lib/data-collector/args.d.ts +6 -24
  55. package/lib/data-collector/args.js +6 -24
  56. package/lib/data-collector/dataset-tools.d.ts +7 -1
  57. package/lib/data-collector/dataset-tools.js +27 -56
  58. package/lib/data-collector/describe.d.ts +8 -33
  59. package/lib/data-collector/describe.js +17 -70
  60. package/lib/data-collector/hub.d.ts +27 -85
  61. package/lib/data-collector/hub.js +19 -85
  62. package/lib/data-collector/query-time.d.ts +7 -26
  63. package/lib/data-collector/query-time.js +8 -27
  64. package/lib/data-collector/store.d.ts +40 -117
  65. package/lib/data-collector/store.js +32 -111
  66. package/lib/data-collector/time-axis.d.ts +16 -76
  67. package/lib/data-collector/time-axis.js +25 -111
  68. package/lib/data-collector/tools.d.ts +6 -6
  69. package/lib/data-collector/tools.js +34 -43
  70. package/lib/dataset-read-entry.d.ts +9 -0
  71. package/lib/dataset-read-entry.js +7 -0
  72. package/lib/index.d.ts +7 -117
  73. package/lib/index.js +43 -422
  74. package/lib/net/eastmoney-client.d.ts +13 -65
  75. package/lib/net/eastmoney-client.js +12 -53
  76. package/lib/net/throttle.d.ts +2 -13
  77. package/lib/net/throttle.js +3 -14
  78. package/lib/ocr/client.d.ts +12 -21
  79. package/lib/ocr/client.js +23 -40
  80. package/lib/ocr/markdown.d.ts +20 -26
  81. package/lib/ocr/markdown.js +23 -36
  82. package/lib/ocr/store.d.ts +14 -47
  83. package/lib/ocr/store.js +15 -45
  84. package/lib/ocr-entry.d.ts +9 -0
  85. package/lib/ocr-entry.js +10 -0
  86. package/lib/policy-entry.d.ts +17 -0
  87. package/lib/policy-entry.js +22 -0
  88. package/lib/preset-claim.d.ts +22 -0
  89. package/lib/preset-claim.js +24 -0
  90. package/lib/preset-ids.d.ts +38 -0
  91. package/lib/preset-ids.js +46 -0
  92. package/lib/retriever-config.d.ts +37 -0
  93. package/lib/retriever-config.js +36 -0
  94. package/lib/retriever-runtime.d.ts +31 -0
  95. package/lib/retriever-runtime.js +82 -0
  96. package/lib/root-extras-entry.d.ts +17 -0
  97. package/lib/root-extras-entry.js +25 -0
  98. package/lib/runtime-entry.d.ts +15 -0
  99. package/lib/runtime-entry.js +5 -0
  100. package/lib/runtime.d.ts +21 -0
  101. package/lib/runtime.js +33 -0
  102. package/lib/search-entry.d.ts +9 -0
  103. package/lib/search-entry.js +9 -0
  104. package/lib/sources/eastmoney-http.js +20 -68
  105. package/lib/sources/ftshare-rest.d.ts +6 -31
  106. package/lib/sources/ftshare-rest.js +1194 -225
  107. package/lib/sources/fuyao-core.d.ts +8 -18
  108. package/lib/sources/fuyao-core.js +10 -21
  109. package/lib/sources/fuyao-rest.d.ts +2 -6
  110. package/lib/sources/fuyao-rest.js +23 -102
  111. package/lib/sources/security-code.d.ts +6 -12
  112. package/lib/sources/security-code.js +6 -12
  113. package/lib/sources/security-identity.d.ts +1 -4
  114. package/lib/sources/security-identity.js +3 -8
  115. package/lib/sources/tencent-http.js +14 -28
  116. package/lib/sources/tencent-public-core.d.ts +8 -60
  117. package/lib/sources/tencent-public-core.js +12 -77
  118. package/lib/sources/wind-mcp.d.ts +0 -5
  119. package/lib/sources/wind-mcp.js +23 -78
  120. package/lib/sources-entry.d.ts +9 -0
  121. package/lib/sources-entry.js +9 -0
  122. package/lib/time/tools.d.ts +8 -31
  123. package/lib/time/tools.js +39 -64
  124. package/lib/time-entry.d.ts +9 -0
  125. package/lib/time-entry.js +7 -0
  126. package/lib/tool-exec.d.ts +45 -11
  127. package/lib/tool-exec.js +59 -6
  128. package/lib/user-customization.d.ts +14 -0
  129. package/lib/user-customization.js +47 -0
  130. package/lib/watchlist/tools.d.ts +3 -13
  131. package/lib/watchlist/tools.js +10 -30
  132. package/lib/watchlist-entry.d.ts +9 -0
  133. package/lib/watchlist-entry.js +6 -0
  134. package/lib/web-retriever/engines.d.ts +6 -11
  135. package/lib/web-retriever/engines.js +9 -14
  136. package/lib/web-retriever/html-markdown.d.ts +14 -34
  137. package/lib/web-retriever/html-markdown.js +39 -75
  138. package/lib/web-retriever/local-fetch.d.ts +22 -58
  139. package/lib/web-retriever/local-fetch.js +33 -77
  140. package/lib/web-retriever/retriever.d.ts +6 -18
  141. package/lib/web-retriever/retriever.js +7 -7
  142. package/lib/web-retriever/sources.d.ts +37 -124
  143. package/lib/web-retriever/sources.js +64 -196
  144. package/lib/web-retriever/tools.d.ts +43 -27
  145. package/lib/web-retriever/tools.js +99 -137
  146. package/lib/web-retriever/wind-client.d.ts +14 -31
  147. package/lib/web-retriever/wind-client.js +16 -32
  148. package/locale/zh.json +1 -1
  149. package/package.json +67 -7
  150. package/preset/capital-generation/README.md +87 -53
  151. package/preset/capital-generation/agent.patch.yml +526 -337
  152. package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
  153. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
  154. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -1
  155. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
  156. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -19
  157. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
  158. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +9 -12
  159. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
  160. package/watchlist/client.js +15 -148
  161. package/watchlist/client.src.cjs +148 -662
  162. package/watchlist/holding-rules.js +11 -38
  163. package/watchlist/index.js +127 -397
  164. package/watchlist/package.json +2 -2
package/config/index.js CHANGED
@@ -4,28 +4,10 @@ import { SELECTED_SKILL_CATALOG, SELECTED_SKILL_SKILL_DIRS } from '../selected-s
4
4
  export const name = 'capital-config'
5
5
 
6
6
  /**
7
- * 本包在 profile 里的**条目 id**。0.1.7 起它就是 settings 命名空间本身:
8
- * `dsh-settings` 不再接受 `settings.register()`,而是把**活动 profile 条目**的 Config schema 里
9
- * 声明为 `.volatile()` 的字段投影成表单,`ns` 恒等于 `entry.options.id`。两个事实因此承重:
10
- *
11
- * 1. **它是卡片的取数通道**:浏览器半边 `ctx.configForms.get(条目 id)` 与
12
- * `whileServed([条目 id])` 都按它寻址;卡片本身注册进 `plugins.bundle.config`,
13
- * key 是**组合包的包名**(`@v587d/capital-generation`),不是 `<包名>#<行 id>`。
14
- * 条目 id 或包名任一处漂移,表现都是**卡片静默消失**(零报错)。
15
- * 2. **它是取数配置的来源**:主插件 `@v587d/capital-generation` 经
16
- * `settings.describe()` 读同一条目的解析值,拿 `fuyaoCredentialRef` /
17
- * `retriever.credentialRef` / `retriever.windDocs.credentialRef` /
18
- * `retriever.paddleOcr.credentialRef` 这些**引用名**去寻密钥。
19
- *
20
- * 密钥本身恒不进本条目:API Key 写在 credentials 域(`remote.credentials.set`),
21
- * 这里只存引用名。唯一写进配置的偏好是回退开关 `retriever.localFetch.enabled`。
22
- *
23
- * ⛔ 字段必须 `.volatile()`:`volatileForm(schema)` 在没有任何 volatile 字段时返回
24
- * `undefined`,该条目就**不进** describe 镜像——卡片读不到命名空间,写入也会被
25
- * `Config field "…" is not volatile` 拒绝。非 volatile 字段仍是普通配置,只能改 profile 文件。
26
- *
27
- * 浏览器半边镜像了这个 id(`client.src.cjs` 的 `ENTRY_ID`),两处同改由
28
- * `test/capital-config.test.mjs` 钉住。
7
+ * Profile entry id; 0.1.7 makes it the settings namespace itself. The browser half
8
+ * addresses it via `configForms.get(ENTRY_ID)` and the main plugin reads the same
9
+ * entry's `*.credentialRef` names; drift makes the card vanish silently. Every
10
+ * editable field must be `.volatile()` or describe omits it.
29
11
  */
30
12
  export const SETTINGS_ENTRY_ID = 'capital-config'
31
13
 
@@ -36,11 +18,8 @@ const WindDocsSchema = z.object({
36
18
  })
37
19
 
38
20
  /**
39
- * 本地直连回退默认值。必须与主插件 `src/index.ts` 的 `LOCAL_FETCH_DEFAULTS` 逐字一致
40
- * (两处 schema 漂移会被 `test/capital-config.test.mjs` 的 deepEqual 用例抓住)。
41
- * 数值口径对齐官方 `dsh-web-fetch-http`(timeoutMs=30000 / maxBodyChars=100000);
42
- * `maxContentChars` 切在转换前的原始 HTML 上,所以按 HTML 体积给足。
43
- * `userAgent` 默认是产品标识;显式配成空串时消费点回落到插件版本号。
21
+ * Must match `LOCAL_FETCH_DEFAULTS` in `src/index.ts` verbatim (test pins it).
22
+ * `maxContentChars` cuts the pre-conversion HTML, so it is sized by HTML volume.
44
23
  */
45
24
  export const LOCAL_FETCH_DEFAULTS = {
46
25
  enabled: true,
@@ -61,12 +40,8 @@ const LocalFetchSchema = z.object({
61
40
  })
62
41
 
63
42
  /**
64
- * PaddleOCR 文档解析(`ocr` 工具)默认值。必须与主插件 `src/index.ts` 的
65
- * `PADDLE_OCR_DEFAULTS` 逐字一致(漂移被 `test/capital-config.test.mjs` 的 deepEqual 抓住)。
66
- *
67
- * `endpoint` / `model` 留空是把真值让给 `src/ocr/client.ts`;`pollBudgetMs: 0` = 客户端默认的
68
- * 200000 毫秒自有等待预算,必须小于 `ocr` 工具声明的 240000 超时,否则框架先超时、
69
- * 回执里没有 `doc_id` 可续查。
43
+ * Must match `PADDLE_OCR_DEFAULTS` in `src/index.ts` verbatim.
44
+ * `pollBudgetMs: 0` = client default 200000 ms, below the tool's 240000 timeout.
70
45
  */
71
46
  export const PADDLE_OCR_DEFAULTS = {
72
47
  endpoint: '',
@@ -84,15 +59,15 @@ const PaddleOcrSchema = z.object({
84
59
 
85
60
  const SelectedSkillsShape = Object.fromEntries(SELECTED_SKILL_CATALOG.map((entry) => [
86
61
  entry.key,
87
- z.boolean().default(false).volatile().description(`Enable ${entry.name} for the Capital root Agent (experimental; default off)`),
62
+ z.boolean().default(false).volatile().description(`Enable ${entry.name} for the Auto mode root Agent (experimental; default off)`),
88
63
  ]))
89
64
  const SelectedSkillsSchema = z.object(SelectedSkillsShape)
90
65
 
91
66
  const SELECTED_SKILL_DEFAULTS = Object.fromEntries(SELECTED_SKILL_CATALOG.map((entry) => [entry.key, false]))
92
67
 
93
68
  export const Config = z.object({
94
- customPersona: z.string().default('').volatile().description('Capital 模式附加人设文本'),
95
- selectedSkills: SelectedSkillsSchema.default(SELECTED_SKILL_DEFAULTS).description('Explicitly selected community experiments; default off and visible only to the Capital root Agent'),
69
+ customPersona: z.string().default('').volatile().description('自动模式附加人设文本'),
70
+ selectedSkills: SelectedSkillsSchema.default(SELECTED_SKILL_DEFAULTS).description('Explicitly selected community experiments; default off and visible only to the Auto mode root Agent'),
96
71
  fuyaoCredentialRef: z.string().default('FUYAO_API_KEY').volatile().description('Fuyao credentials 引用名'),
97
72
  ftshareCredentialRef: z.string().default('FTSHARE_API_KEY').volatile().description('FTShare(非凸科技)credentials 引用名'),
98
73
  retriever: z.object({
@@ -115,30 +90,17 @@ export const Config = z.object({
115
90
  })
116
91
 
117
92
  /**
118
- * 「精选 Skills」那一格的**原文旁路**:卡片上的「详情」悬浮要显示 `SKILL.md` 原文,
119
- * 而原文是随包携带的快照文件(35 份共 ~750 KB)。
120
- *
121
- * 为什么走一条本机路由,而不是把原文打进客户端 bundle:客户端 bundle 在页面加载那一刻
122
- * 整体送达浏览器(`window.__ModuleLoader__.load` 是单文件 IIFE,没有按需分片),750 KB 会
123
- * 让每一次打开插件页都付一遍——包括从不点「详情」的那次。旁路只在悬停时取一份,且取的是
124
- * **磁盘上当前那份**,不是构建期抄的副本:抄来的原文会随快照更新而变旧,而这块 UI 说的
125
- * 是"这就是原文"(控件对自己的状态说谎 = 事故)。
126
- *
127
- * 闭集:路径按 `SELECTED_SKILL_CATALOG` × `SELECTED_SKILL_SKILL_DIRS` 在模块加载时现算成
128
- * `Map<skill 名, file: URL>`,请求里的名字**只做这张表的键**,不参与任何路径拼接——
129
- * `%2F..%2F` 之类一律落在"表里没有这个名字"那一侧(404)。路径用 `URL` 表示:`fs` 直接收
130
- * `file:` URL,省掉一次 `fileURLToPath`(也就省掉在 import 期依赖 node 内置模块)。
131
- *
132
- * ⛔ 认证围栏与 `/capital-charts`、`/capital-watchlist` 同一写法(`connection.requestRejection`,
133
- * docs/dev/chart-presentation.md §6.1):`webServer.register({ kind: 'prefix' })` 本身不做任何认证。
93
+ * Read-only route for the "Selected Skills" detail hover: serves the packaged
94
+ * SKILL.md snapshot from disk instead of shipping ~750 KB in the client bundle.
95
+ * Request names only index a precomputed Map (never path fragments). Same auth
96
+ * fence as `/capital-charts` (`requestRejection`).
134
97
  */
135
98
  export const SKILL_DOC_ROUTE = '/capital-skills'
136
- /** 单份原文的字节上限:超限**整份拒绝、绝不截断**("原文"被切一半比没有原文更误导人)。 */
99
+ /** Byte cap per document: oversized files are rejected whole, never truncated. */
137
100
  export const MAX_SKILL_DOC_BYTES = 128 * 1024
138
101
 
139
102
  const SKILL_DOC_SUFFIX = 'SKILL.md'
140
103
 
141
- /** catalog 里的顺序 = 卡片上的顺序,所以这里只多算一份"名字 → 快照文件"的索引。 */
142
104
  export const SKILL_DOCS = new Map(SELECTED_SKILL_CATALOG.map((entry) => [
143
105
  entry.name,
144
106
  new URL(`../selected-skills/${entry.repository}/${SELECTED_SKILL_SKILL_DIRS[entry.repository]}/${entry.name}/${SKILL_DOC_SUFFIX}`, import.meta.url),
@@ -159,10 +121,7 @@ function sendJson(res, status, payload) {
159
121
  res.end(JSON.stringify(payload))
160
122
  }
161
123
 
162
- /**
163
- * @param options.readFile - `(url) => string | Buffer`,用例注入假读盘。
164
- * @param options.authorize - `(req) => 401 | 403 | undefined`;非空即拒,**且一次都不碰文件系统**。
165
- */
124
+ /** `options.authorize` = `(req) => 401 | 403 | undefined`; non-empty rejects without touching fs. */
166
125
  export function createSkillDocHandler(options = {}) {
167
126
  const readFile = options.readFile ?? ((target) => fsModule().readFileSync(target, 'utf8'))
168
127
  const statSize = options.statSize ?? ((target) => fsModule().statSync(target).size)
@@ -173,7 +132,7 @@ export function createSkillDocHandler(options = {}) {
173
132
  try {
174
133
  rejection = authorize(req)
175
134
  } catch {
176
- // 围栏自己答不上来时必须 fail closed:放行就等于"认证失败 = 不认证"。
135
+ // Fail closed: an authorizer that cannot answer must not mean "no auth".
177
136
  rejection = 401
178
137
  }
179
138
  if (rejection !== undefined && rejection !== null) {
@@ -220,7 +179,7 @@ export function createSkillDocHandler(options = {}) {
220
179
  try {
221
180
  bytes = statSize(target)
222
181
  } catch {
223
- // 快照没随包落地(装到一半 / files 清单漏了):说清楚"读不到",别回一份空原文当好结果。
182
+ // Snapshot missing from the package: say unreadable, never an empty document.
224
183
  sendJson(res, 404, { error: 'skill_doc_unreadable', name })
225
184
  return
226
185
  }
@@ -243,23 +202,14 @@ export function createSkillDocHandler(options = {}) {
243
202
  }
244
203
  }
245
204
 
246
- /**
247
- * 卡片那侧拼 URL 的唯一口径(文件名里带 `.` 的 slug 也要能拼,所以走 encodeURIComponent)。
248
- * 客户端 bundle 与这里是**同一条字符串**的两端:漂移的表现为"详情永远读不到",由
249
- * `test/capital-config.test.mjs` 的同字断言钉住(客户端不能 import 本模块 ⇒ 只能对着产物量)。
250
- */
205
+ /** URL built identically in the client bundle; drift = details never load (test pins both). */
251
206
  export function skillDocUrl(name) {
252
207
  return `${SKILL_DOC_ROUTE}/${encodeURIComponent(name)}.md`
253
208
  }
254
209
 
255
210
  /**
256
- * 本行消费两件事:自己的配置(schema 事实,见上)与快照原文的读盘旁路。
257
- *
258
- * `webServer` 只走 `inject`——`ctx.get('webServer')` 解析得到值**不代表**属性访问合法
259
- * (cordis 的属性代理只认"本层声明过 inject"),探测式写法会把正确性押在装配顺序上,
260
- * 这条踩过两次(见 `chart-ui/index.js` 的同一段注释)。认证面 `connection` 反之要**惰性**
261
- * 解析:它缺席(Electron / file:// 这类没有浏览器认证面的载体)就退化成"没有认证面"的旧行为,
262
- * 但解析抛错一律 401。
211
+ * `webServer` only via inject (probing `ctx.webServer` bets on mount order);
212
+ * `connection` resolved lazily: absent = no auth plane, throwing = 401.
263
213
  */
264
214
  export function apply(ctx) {
265
215
  const authorize = (req) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@v587d/config",
3
- "version": "2.8.2",
4
- "description": "Capital 模式的取数凭据与偏好配置行:卡片坐在 @v587d/capital-generation 插件主页的「配置」段,点这一行本身没有表单。",
3
+ "version": "2.9.0",
4
+ "description": "自动模式的取数凭据与偏好配置行:卡片坐在 @v587d/capital-generation 插件主页的「配置」段,点这一行本身没有表单。",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
package/cordis.patch.yml CHANGED
@@ -1,42 +1,29 @@
1
1
  # Capital Generation is a distributable DSH profile bundle.
2
- #
3
- # 本文件只挂 **host 平面** 的客户端 bundle 行(下面三个 insert)。预设不在这里:0.1.7 起
4
- # 预设是普通条目行,本包的预设声明住在 `preset/capital-generation/agent.patch.yml`,由
5
- # package.json 的 `dsh.bundle.patch` 数组按序应用(`agent-preset-registry` 行由 dsh-web-app
6
- # 自己提供,我们只 insert 自己的预设声明,不动它的 config)。
7
- #
8
- # ⛔ 升级事故(2026-09-26,issue #3):这里原先是 patch `agent-presets` 行 + `config.roots`
9
- # 的目录式注册。0.1.7 把该条目改名为 `agent-preset-registry` 并**删掉了 roots 机制**(registry
10
- # 「既不扫目录也不接受预设路径」),于是每次启动静默报
11
- # `dsh: [@v587d/capital-generation] patch: entry "agent-presets" not found` —— 预设永不注册,
12
- # 历史会话(头部记着 `"agentPreset":"capital-generation"`)全部 `Unknown agent preset` 读不回。
2
+ # This file mounts host-plane rows only (the inserts below). Presets are ordinary entry rows and
3
+ # live in preset/capital-generation/agent.patch.yml, applied in order by package.json's
4
+ # `dsh.bundle.patch`; we insert our own preset declaration and never touch the registry's config.
5
+ # ⛔ Do not register presets by patching an `agent-presets` row's `config.roots`: 0.1.7 renamed the
6
+ # entry to `agent-preset-registry` and deleted the roots mechanism. The failure is a single boot warn
7
+ # (`patch: entry "agent-presets" not found`), so presets never register and every saved session
8
+ # reopening with `"agentPreset":"capital-generation"` reports `Unknown agent preset`.
13
9
 
14
- # ── 图表前端组件(host 平面行)────────────────────────────────────────────────
15
- #
16
- # 为什么这一行必须在这里、必须是 host 平面:客户端 bundle 的注入发生在
17
- # index.html 渲染那一刻,运行期新增 entry 没有送达浏览器的通道;挂在 preset 行上的
18
- # 客户端 bundle 到不了已经打开的页面。见 chart-ui/README.md。
10
+ # ── Chart frontend (host plane) ────────────────────────────────────────────────
11
+ # Host plane because client bundles are injected at index.html render time; a bundle mounted on a
12
+ # preset row never reaches an already-open page (see chart-ui/README.md).
13
+ # `name` must be a literal, path-like relative path — three pitfalls, each hit for real:
14
+ # 1. only `config` is interpolated, so `!!js` in `name` reaches the ESM import verbatim
15
+ # (`name.startsWith is not a function`, whole profile fails to boot).
16
+ # 2. a subpath specifier (`@v587d/capital-generation/chart-ui`) is neither path-like nor a
17
+ # resolvable package name, so locatePkgJson silently classifies the row as "not a client
18
+ # bundle": the bundle never enters __DSH_BOOT__, the UI is missing, nothing is logged.
19
+ # 3. the resolution base is this file's directory, not the profile root, so `./chart-ui/index.js`
20
+ # works under any node_modules layout; package identity comes from chart-ui/package.json
21
+ # (@v587d/capital-charts), which is why it cannot collide with the main package.
22
+ # Regression: test/chart-composition.test.mjs (name stays a literal path-like string and resolves).
19
23
  #
20
- # 为什么行名是**相对本包目录的路径**(三个坑都踩过,都不是猜的):
21
- # 1. **`name` 不接受 `!!js`**:loader 只对 fiber 的 `config` 做插值
22
- # (cordis-plugin-loader 的 `internal/config` 钩子),`name` 永远按字面量送进 ESM import。
23
- # 写过 !!js 的后果是启动期 `name.startsWith is not a function`——整个 profile 起不来。
24
- # 2. **`name` 也不能是 subpath 说明符**(如 `@v587d/capital-generation/chart-ui`):
25
- # dsh-client-modules 的 locatePkgJson 里
26
- # `if (!pathLike && exactPackageSpecifier(loaderName) === undefined) return undefined`
27
- # —— subpath 既非 path-like、打包名解析又返回 undefined,于是这颗行被**静默**当成
28
- # "不是客户端包",bundle 永不进 __DSH_BOOT__(表现是图表 UI 不出现,且不报错)。
29
- # 3. 解析基址是**本 patch 文件所在目录**,不是 profile 根:
30
- # cordis-plugin-include 装载 patch 时会 `ctx.baseUrl = new URL('.', pathToFileURL(filename))`。
31
- # 所以这里的相对路径就是本包目录下的 chart-ui/index.js —— 与 profile 的 node_modules
32
- # 布局无关(pnpm 的 isolated 布局也照常工作)。
33
- # locatePkgJson 随后按"最近的 package.json"归属,正好是 chart-ui/package.json,
34
- # 包身份为 @v587d/capital-charts,与主包不撞名。
35
- # 回归断言见 test/chart-composition.test.mjs(name 必须是字面量且 path-like,并真的解析一次)。
36
- #
37
- # ⚠ 目录名 / 包名与**行 id 是两条独立的链**(AGENTS.md §5.1):下面两颗行住在 `config/` 与
38
- # `watchlist/`,行 id 却仍是 `capital-config` / `capital-watchlist`——后者是 settings 命名空间与
39
- # 「包含的组件」里那两行的身份,改了就会吃掉用户已保存的配置。不许"顺手对齐"。
24
+ # ⚠ Directory name and row id are two independent chains (AGENTS.md §5.1): the rows below live in
25
+ # `config/` and `watchlist/`, yet keep ids `capital-config` / `capital-watchlist` — those ids are
26
+ # the settings namespace and the identity of the user's saved config; "aligning" them discards it.
40
27
  - insert:
41
28
  - id: capital-config
42
29
  name: './config/index.js'
@@ -55,22 +42,30 @@
55
42
  - id: capital-charts
56
43
  name: './chart-ui/index.js'
57
44
 
58
- # ── 用户自选股(host 平面行)──────────────────────────────────────────────────
59
- #
60
- # 为什么也必须在 host 平面:自选股是**跨 workspace 的用户侧资产**(清单与报价快照落
61
- # `~/.dsh/storages/capital_watchlist.json`),而客户端 bundle 的注入只发生在 index.html
62
- # 渲染那一刻——挂在 preset 行上就到不了已经打开的页面(同 chart-ui 的理由)。
63
- #
64
- # 与上一颗 preset 行的方向是**反的**:chart-ui 的数据由插件侧 push 进来(`ctx.get` 只能
65
- # 从会话作用域向外解析),本行则要在 host 平面自己出网,所以它 import
66
- # `../lib/sources/fuyao-core.js`(零 import 的叶子内核)——**同一份** key 解析 / 信封判定 /
67
- # 错误映射,不许有第二份(AGENTS.md §9.7)。这条跨包相对导入在本仓没有先例,因此由
68
- # `test/watchlist-quote.test.mjs` 的**模块同一性探针**钉住:两处 import 必须拿到同一实例,
69
- # 探针不过就重新设计,而不是先上线再说。
70
- #
71
- # `id` 与 `./watchlist/index.js` 的 path-like 字面量规则同上(!!js / subpath 两个坑
72
- # 对这颗行同样成立)。
45
+ # ── User watchlist (host plane) ───────────────────────────────────────────────
46
+ # Cross-workspace user asset (~/.dsh/storages/capital_watchlist.json), so host plane for the same
47
+ # injection reason as chart-ui; the `name` rules above apply here too.
48
+ # Direction is the opposite of chart-ui's: this row reaches the network itself, so it imports
49
+ # ../lib/sources/fuyao-core.js (a zero-import leaf) rather than a second copy of key resolution,
50
+ # envelope checks and error mapping (AGENTS.md §9.7). No precedent for that cross-package relative
51
+ # import here, so test/watchlist-quote.test.mjs pins module identity: both import sites must get
52
+ # the same instance, and if the probe fails the design is wrong — don't ship it and hope.
73
53
  - insert:
74
54
  - id: capital-watchlist
75
55
  name: './watchlist/index.js'
76
56
 
57
+ # ── Cross-preset runtime / policy (0.2.1) ─────────────────────────────────────
58
+ # Root and role presets mount separately, so apply() runs several times per process. Shared state
59
+ # (chart source tokens, chart artifacts) and the agent/created listeners (root convergence, shell
60
+ # and fs guards) must exist exactly once, which only a host-plane provide gives.
61
+ # These two rows use package subpath entries (@v587d/capital-generation/runtime and /policy): they
62
+ # are server-side, so the literal-path rule above doesn't apply — DSH composes its own
63
+ # @deepseek-ai/dsh-tool-subagent-control rows the same way.
64
+ - insert:
65
+ - id: capital-runtime
66
+ name: '@v587d/capital-generation/runtime'
67
+
68
+ - insert:
69
+ - id: capital-policy
70
+ name: '@v587d/capital-generation/policy'
71
+
@@ -1,60 +1,14 @@
1
- /**
2
- * shell 调用闸门(`bash` / `pwsh` 两个名字都认,只拦**必然失败的沙箱升级**,不看命令内容)。
3
- *
4
- * 背景与边界(设计理由全文在 preset/capital-generation/README.md):
5
- * - bash 不是计算器,是一台完整终端。本仓实测:沙箱**只拦写**(workspace + /tmp 之外
6
- * 一律 EROFS),**不拦读**(`~/.dsh/.credentials.yaml`、`sessions/**` 整机可读),
7
- * **不拦网络**(`curl` 直连 200)。这是 DSH 沙箱词汇表的既定边界——`SandboxMode`
8
- * 只覆盖文件效果,官方文档亦如此声明。
9
- * - 因此本文件**不做命令内容匹配**:正则挡不住 `node -e`/`python3 -c`,而 node/python
10
- * 正是引入 bash 的目的(未来 data_analyst 也靠它)。禁区与出网纪律走 data_junior 的
11
- * persona 软约束,不在这里伪装成安全边界。
12
- * - 沙箱写边界也无法单独收窄:数据集管线自己就走沙箱,`store.writeContext()` 用**调用方
13
- * session** 的 policy 且要求 mode ∈ {workspace-write, danger-full-access},把 data_junior
14
- * 的会话钉成 read-only 会直接打断 `describe_dataset` 的 profile 落盘
15
- * (`workspace_not_writable`)。bash 与 Dataset 管线共用同一把尺子(session policy)。
16
- *
17
- * 规则只留一层(2026-10-15 用户决策「开 read、同时保留 bash」后):
18
- * A 结构层(**已随决策移除**):曾拒绝根会话的 shell 调用("只对被委派子会话开放")。
19
- * 现在主 Agent 保留 bash / pwsh(读 skill 分册、纯计算);shell 归谁由可见性收敛——
20
- * `ROOT_AGENT_DENIED_TOOLS` 不再含 shell 名,子会话侧由委派行的 `toolFilter` 把 shell
21
- * 只给 data_junior。迁移的断言在 `test/bash-guard.test.mjs`(根会话放行是**新契约**)。
22
- * B UX 层(保留,收窄到"被委派 / 身份不可考的调用方"):委派子会话的审批策略由框架固定为
23
- * `never`(dsh-subagent 的 `captureDelegatedPolicyOverrides` → `approval/policy: never`,
24
- * `dsh-user-approval.decide()` 在 never 时**直接返回 rejected、根本不调用 answerer**),
25
- * 所以 `sandbox_permissions` 必然失败,而且模型会收到「the user rejected…」这种
26
- * **把系统拒绝说成用户拒绝**的文案。这里提前拦下并给出正确路径:省一次必然白跑的往返 +
27
- * 纠正归因。主 Agent 自己的升级照常走 ask 审批(那正是 escalation 的正当用途)。
28
- *
29
- * 为什么用 `tools.guard` 而不是 `tools/pre-execute`:guard 是**单调**的(没有 allow 结果,
30
- * 任何监听顺序都无法把别人的 deny 翻回 allow),同步、拿到解析后的 `arguments`,且
31
- * 「注册在 agent.ctx 上就只作用于该 agent」。pre-execute 是 waterfall,可扩展也意味着可被后续
32
- * 监听改写。
33
- */
1
+ /** Shell guard for bash/pwsh: blocks only doomed sandbox escalation; command content is never
2
+ * inspected (the sandbox blocks writes, not reads/network). Delegated approvals are fixed to
3
+ * `never`, so escalation auto-rejects with a misleading "user rejected"; block early instead.
4
+ * Root escalation still uses ask; do not pin delegated sessions read-only (`store.writeContext`
5
+ * needs workspace-write). `tools.guard` is monotonic; pre-execute is a rewritable waterfall. */
34
6
  import { type GuardedExecution, type PolicyContext } from './root-tool-policy.js';
35
- /** 闸门看到的调用形状:guard 拿到的是解析后的 arguments,不是一个 shell 字符串。 */
36
7
  export type BashGuardExecution = GuardedExecution;
37
- /**
38
- * 沙箱升级拒绝文案:说清"这不是用户拒绝",并给出正确路径。
39
- * (A 结构层文案 `SHELL_DELEGATED_ONLY` 随 2026-10-15 决策删除,断言迁移到
40
- * `test/bash-guard.test.mjs` 的"根会话放行"用例。)
41
- */
8
+ /** States that the rejection is the system's, not the user's, and gives the recovery path. */
42
9
  export declare const SHELL_ESCALATION_UNREACHABLE: string;
43
- /**
44
- * 单次 shell 调用的判定:返回拒绝理由,或 undefined 放行。
45
- *
46
- * 2026-10-15 起主 Agent 的 shell 调用一律放行(A 层随决策移除);被拦的只有**升级参数**
47
- * ——注意这里**只读两个字段**(调用方身份、`sandbox_permissions`/`justification`),
48
- * 绝不解析 `command`——理由见文件头。升级判不了身份时 fail closed:根会话的形状是
49
- * "有 session 且无 parentSession",证明不了就当最坏打算,不给"拿不到身份就放行"的口子。
50
- */
10
+ /** Reads identity plus `sandbox_permissions`/`justification`, never `command`; unknown identity fails closed. */
51
11
  export declare function bashGuardReason(exec: BashGuardExecution | undefined): string | undefined;
52
- /**
53
- * 注册 shell 闸门:对**本 preset 的每个 agent**(含主 Agent)在它自己的 scope 上装一次 guard。
54
- *
55
- * 监听位置与 `registerRootToolPolicy` 同因:`agent/created` 的路由键是 agent 对象本身,
56
- * 注册在 standing scope(打了标签)的监听器**收不到且不报错**,必须挂 `ctx.root`。
57
- * 安装/释放都不得抛回 agent 创建路径——宁可少一层闸门,也不能挡住建 agent;
58
- * 静默失败会留下诊断 warn(见 `registerBashGuard` 的返回值处理)。
59
- */
12
+ /** Installs on every Capital preset agent. Must listen on `ctx.root`: agent/created routes by
13
+ * the agent object, so tagged standing-scope listeners never fire. Failures only warn. */
60
14
  export declare function registerBashGuard(ctx: PolicyContext): void;
@@ -1,64 +1,21 @@
1
- /**
2
- * shell 调用闸门(`bash` / `pwsh` 两个名字都认,只拦**必然失败的沙箱升级**,不看命令内容)。
3
- *
4
- * 背景与边界(设计理由全文在 preset/capital-generation/README.md):
5
- * - bash 不是计算器,是一台完整终端。本仓实测:沙箱**只拦写**(workspace + /tmp 之外
6
- * 一律 EROFS),**不拦读**(`~/.dsh/.credentials.yaml`、`sessions/**` 整机可读),
7
- * **不拦网络**(`curl` 直连 200)。这是 DSH 沙箱词汇表的既定边界——`SandboxMode`
8
- * 只覆盖文件效果,官方文档亦如此声明。
9
- * - 因此本文件**不做命令内容匹配**:正则挡不住 `node -e`/`python3 -c`,而 node/python
10
- * 正是引入 bash 的目的(未来 data_analyst 也靠它)。禁区与出网纪律走 data_junior 的
11
- * persona 软约束,不在这里伪装成安全边界。
12
- * - 沙箱写边界也无法单独收窄:数据集管线自己就走沙箱,`store.writeContext()` 用**调用方
13
- * session** 的 policy 且要求 mode ∈ {workspace-write, danger-full-access},把 data_junior
14
- * 的会话钉成 read-only 会直接打断 `describe_dataset` 的 profile 落盘
15
- * (`workspace_not_writable`)。bash 与 Dataset 管线共用同一把尺子(session policy)。
16
- *
17
- * 规则只留一层(2026-10-15 用户决策「开 read、同时保留 bash」后):
18
- * A 结构层(**已随决策移除**):曾拒绝根会话的 shell 调用("只对被委派子会话开放")。
19
- * 现在主 Agent 保留 bash / pwsh(读 skill 分册、纯计算);shell 归谁由可见性收敛——
20
- * `ROOT_AGENT_DENIED_TOOLS` 不再含 shell 名,子会话侧由委派行的 `toolFilter` 把 shell
21
- * 只给 data_junior。迁移的断言在 `test/bash-guard.test.mjs`(根会话放行是**新契约**)。
22
- * B UX 层(保留,收窄到"被委派 / 身份不可考的调用方"):委派子会话的审批策略由框架固定为
23
- * `never`(dsh-subagent 的 `captureDelegatedPolicyOverrides` → `approval/policy: never`,
24
- * `dsh-user-approval.decide()` 在 never 时**直接返回 rejected、根本不调用 answerer**),
25
- * 所以 `sandbox_permissions` 必然失败,而且模型会收到「the user rejected…」这种
26
- * **把系统拒绝说成用户拒绝**的文案。这里提前拦下并给出正确路径:省一次必然白跑的往返 +
27
- * 纠正归因。主 Agent 自己的升级照常走 ask 审批(那正是 escalation 的正当用途)。
28
- *
29
- * 为什么用 `tools.guard` 而不是 `tools/pre-execute`:guard 是**单调**的(没有 allow 结果,
30
- * 任何监听顺序都无法把别人的 deny 翻回 allow),同步、拿到解析后的 `arguments`,且
31
- * 「注册在 agent.ctx 上就只作用于该 agent」。pre-execute 是 waterfall,可扩展也意味着可被后续
32
- * 监听改写。
33
- */
34
- import { agentSessionId, isCapitalAgent, SHELL_TOOL_NAMES } from './root-tool-policy.js';
35
- /**
36
- * shell 工具名全集(`bash` / `pwsh`,定义只在 {@link SHELL_TOOL_NAMES} 一份)。
37
- *
38
- * ⛔ 这里不能按 `process.platform` 挑一个:preset 里那两行是平台成对挂载的,Windows 上
39
- * 模型调的是 `pwsh`,而闸门跑在**同一个进程**里——只认 `bash` 的话,两层判定在 Windows 上
40
- * 一次都不触发,且不报错(2026-09-30 实测发现,症状是"§1.6 那道闸门在 Windows 上是死代码")。
41
- */
1
+ /** Shell guard for bash/pwsh: blocks only doomed sandbox escalation; command content is never
2
+ * inspected (the sandbox blocks writes, not reads/network). Delegated approvals are fixed to
3
+ * `never`, so escalation auto-rejects with a misleading "user rejected"; block early instead.
4
+ * Root escalation still uses ask; do not pin delegated sessions read-only (`store.writeContext`
5
+ * needs workspace-write). `tools.guard` is monotonic; pre-execute is a rewritable waterfall. */
6
+ import { agentSessionId, isCapitalPresetAgent, SHELL_TOOL_NAMES } from './root-tool-policy.js';
7
+ /** Both names on every platform: the preset mounts a pair, so matching only the local platform kills the Windows gate. */
42
8
  const shellToolNames = new Set(SHELL_TOOL_NAMES);
43
- /**
44
- * 沙箱升级拒绝文案:说清"这不是用户拒绝",并给出正确路径。
45
- * (A 结构层文案 `SHELL_DELEGATED_ONLY` 随 2026-10-15 决策删除,断言迁移到
46
- * `test/bash-guard.test.mjs` 的"根会话放行"用例。)
47
- */
9
+ /** One listener set per root context: host policy and legacy call sites install once. */
10
+ const installedRoots = new WeakSet();
11
+ /** States that the rejection is the system's, not the user's, and gives the recovery path. */
48
12
  export const SHELL_ESCALATION_UNREACHABLE = 'shell 工具(bash / pwsh)的沙箱升级只有主 Agent 的会话能拿到用户审批:被委派的子会话拿不到'
49
13
  + '(框架把委派会话的审批策略固定为 never,不会去问用户),当前调用方也不是可确认的主 Agent 会话。'
50
14
  + '不要重试升级:缺数值用 describe_dataset / query_dataset,缺数据用 send_message 发 data_gap 回主 Agent。';
51
15
  function isRecord(value) {
52
16
  return typeof value === 'object' && value !== null && !Array.isArray(value);
53
17
  }
54
- /**
55
- * 单次 shell 调用的判定:返回拒绝理由,或 undefined 放行。
56
- *
57
- * 2026-10-15 起主 Agent 的 shell 调用一律放行(A 层随决策移除);被拦的只有**升级参数**
58
- * ——注意这里**只读两个字段**(调用方身份、`sandbox_permissions`/`justification`),
59
- * 绝不解析 `command`——理由见文件头。升级判不了身份时 fail closed:根会话的形状是
60
- * "有 session 且无 parentSession",证明不了就当最坏打算,不给"拿不到身份就放行"的口子。
61
- */
18
+ /** Reads identity plus `sandbox_permissions`/`justification`, never `command`; unknown identity fails closed. */
62
19
  export function bashGuardReason(exec) {
63
20
  if (exec === undefined || typeof exec.name !== 'string' || !shellToolNames.has(exec.name))
64
21
  return undefined;
@@ -70,7 +27,6 @@ export function bashGuardReason(exec) {
70
27
  const knownRoot = exec.agent?.session !== undefined && (typeof parent !== 'string' || parent.length === 0);
71
28
  return knownRoot ? undefined : SHELL_ESCALATION_UNREACHABLE;
72
29
  }
73
- /** 在某个 agent 自己的 scope 上安装 guard,返回 disposer(不可用时返回 undefined)。 */
74
30
  function installAgentGuard(agent) {
75
31
  let tools;
76
32
  try {
@@ -89,25 +45,22 @@ function installAgentGuard(agent) {
89
45
  return undefined;
90
46
  }
91
47
  }
92
- /**
93
- * 注册 shell 闸门:对**本 preset 的每个 agent**(含主 Agent)在它自己的 scope 上装一次 guard。
94
- *
95
- * 监听位置与 `registerRootToolPolicy` 同因:`agent/created` 的路由键是 agent 对象本身,
96
- * 注册在 standing scope(打了标签)的监听器**收不到且不报错**,必须挂 `ctx.root`。
97
- * 安装/释放都不得抛回 agent 创建路径——宁可少一层闸门,也不能挡住建 agent;
98
- * 静默失败会留下诊断 warn(见 `registerBashGuard` 的返回值处理)。
99
- */
48
+ /** Installs on every Capital preset agent. Must listen on `ctx.root`: agent/created routes by
49
+ * the agent object, so tagged standing-scope listeners never fire. Failures only warn. */
100
50
  export function registerBashGuard(ctx) {
101
51
  const listenCtx = ctx.root ?? ctx;
102
52
  if (typeof listenCtx.on !== 'function')
103
53
  return;
104
- /** agent 会话 id → guard 的 disposer;agent/disposed 时释放,避免跨实例残留。 */
54
+ if (installedRoots.has(listenCtx))
55
+ return;
56
+ installedRoots.add(listenCtx);
57
+ /** Session id -> disposer; released on agent/disposed to avoid cross-instance leaks. */
105
58
  const guards = new Map();
106
59
  const install = () => {
107
60
  const stopCreated = listenCtx.on?.('agent/created', (payload) => {
108
61
  try {
109
62
  const agent = payload?.agent;
110
- if (!isCapitalAgent(ctx, agent))
63
+ if (!isCapitalPresetAgent(ctx, agent))
111
64
  return;
112
65
  const dispose = installAgentGuard(agent);
113
66
  if (dispose === undefined) {
@@ -133,7 +86,7 @@ export function registerBashGuard(ctx) {
133
86
  guards.delete(id);
134
87
  dispose();
135
88
  }
136
- catch { /* 释放失败不该影响 agent 拆除路径 */ }
89
+ catch { /* disposal must not break agent teardown */ }
137
90
  });
138
91
  return () => {
139
92
  try {
@@ -158,7 +111,14 @@ export function registerBashGuard(ctx) {
158
111
  if (typeof ctx.effect === 'function') {
159
112
  ctx.effect(() => {
160
113
  const dispose = install();
161
- return () => dispose();
114
+ return () => {
115
+ try {
116
+ dispose();
117
+ }
118
+ finally {
119
+ installedRoots.delete(listenCtx);
120
+ }
121
+ };
162
122
  }, 'capital-generation.bash-guard()');
163
123
  return;
164
124
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hide delegation-only role presets from the host's new-session roster.
3
+ *
4
+ * The registry has no hidden flag. `remoteExportList()` is what the remote `agentPresets/list`
5
+ * endpoint invokes, so wrapping it changes the selector without touching `list()`, `resolve()`,
6
+ * delegation or settings management. Fail open: unknown shapes and missing methods leave the
7
+ * roster untouched, and fiber cleanup restores the original property descriptor.
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ type EffectContext = {
11
+ effect?: (execute: () => () => void, label?: string) => unknown;
12
+ };
13
+ /** Drop internal rows without mutating the caller's array; unknown shapes pass through. */
14
+ export declare function filterPresetRoster(value: unknown): unknown;
15
+ /**
16
+ * Install on one registry instance. Returns false when the host exposes no roster method or the
17
+ * property cannot be shadowed; callers then behave exactly as if this filter did not exist.
18
+ */
19
+ export declare function installPresetRosterFilter(service: unknown, ctx: EffectContext): boolean;
20
+ /** Wait for the registry service, then filter its remote roster. */
21
+ export declare function registerPresetRosterFilter(ctx: Context): void;
22
+ export {};
@@ -0,0 +1,87 @@
1
+ import { isInternalRolePresetId } from '../preset-ids.js';
2
+ // A future host may serve `agentPresets/list` straight from `list`; prefer the dedicated export.
3
+ const METHODS = ['remoteExportList', 'list'];
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null;
6
+ }
7
+ function isInternalRow(row) {
8
+ return isRecord(row) && isInternalRolePresetId(row.id);
9
+ }
10
+ /** Drop internal rows without mutating the caller's array; unknown shapes pass through. */
11
+ export function filterPresetRoster(value) {
12
+ try {
13
+ if (Array.isArray(value))
14
+ return value.filter((row) => !isInternalRow(row));
15
+ if (!isRecord(value) || !Array.isArray(value.presets))
16
+ return value;
17
+ return { ...value, presets: value.presets.filter((row) => !isInternalRow(row)) };
18
+ }
19
+ catch {
20
+ return value;
21
+ }
22
+ }
23
+ function pickMethod(service) {
24
+ return METHODS.find((method) => typeof service[method] === 'function');
25
+ }
26
+ /**
27
+ * Install on one registry instance. Returns false when the host exposes no roster method or the
28
+ * property cannot be shadowed; callers then behave exactly as if this filter did not exist.
29
+ */
30
+ export function installPresetRosterFilter(service, ctx) {
31
+ if (!isRecord(service))
32
+ return false;
33
+ const method = pickMethod(service);
34
+ if (method === undefined)
35
+ return false;
36
+ const original = service[method];
37
+ if (typeof original !== 'function')
38
+ return false;
39
+ const previous = Object.getOwnPropertyDescriptor(service, method);
40
+ const wrapper = (...args) => {
41
+ const result = original.call(service, ...args);
42
+ try {
43
+ if (isRecord(result) && typeof result.then === 'function') {
44
+ return result.then((value) => filterPresetRoster(value));
45
+ }
46
+ return filterPresetRoster(result);
47
+ }
48
+ catch {
49
+ return result;
50
+ }
51
+ };
52
+ try {
53
+ Object.defineProperty(service, method, {
54
+ configurable: true,
55
+ enumerable: previous?.enumerable ?? false,
56
+ writable: true,
57
+ value: wrapper,
58
+ });
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ const restore = () => {
64
+ if (service[method] !== wrapper)
65
+ return;
66
+ try {
67
+ if (previous === undefined)
68
+ delete service[method];
69
+ else
70
+ Object.defineProperty(service, method, previous);
71
+ }
72
+ catch {
73
+ }
74
+ };
75
+ if (typeof ctx.effect === 'function')
76
+ ctx.effect(() => restore, 'capital-preset-roster');
77
+ return true;
78
+ }
79
+ /** Wait for the registry service, then filter its remote roster. */
80
+ export function registerPresetRosterFilter(ctx) {
81
+ const host = ctx;
82
+ if (typeof host.inject !== 'function')
83
+ return;
84
+ host.inject(['agentPresets'], (injected) => {
85
+ installPresetRosterFilter(injected.agentPresets, injected);
86
+ });
87
+ }