@v587d/capital-generation 0.0.0-stage → 2.6.1

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 (117) hide show
  1. package/CHANGELOG.md +814 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -2
  4. package/assets/cg-icon.png +0 -0
  5. package/capital-config/client.js +28 -0
  6. package/capital-config/client.src.cjs +388 -0
  7. package/capital-config/index.js +113 -0
  8. package/capital-config/package.json +36 -0
  9. package/capital-watchlist/client.js +182 -0
  10. package/capital-watchlist/client.src.cjs +1229 -0
  11. package/capital-watchlist/index.js +772 -0
  12. package/capital-watchlist/package.json +37 -0
  13. package/chart-ui/README.md +112 -0
  14. package/chart-ui/client.js +16 -0
  15. package/chart-ui/client.src.cjs +126 -0
  16. package/chart-ui/index.js +205 -0
  17. package/chart-ui/package.json +30 -0
  18. package/cordis.patch.yml +72 -0
  19. package/lib/agents/bash-guard.d.ts +55 -0
  20. package/lib/agents/bash-guard.js +162 -0
  21. package/lib/agents/root-tool-policy.d.ts +164 -0
  22. package/lib/agents/root-tool-policy.js +309 -0
  23. package/lib/chart/artifact-ref.d.ts +65 -0
  24. package/lib/chart/artifact-ref.js +107 -0
  25. package/lib/chart/downsample.d.ts +8 -0
  26. package/lib/chart/downsample.js +53 -0
  27. package/lib/chart/errors.d.ts +16 -0
  28. package/lib/chart/errors.js +15 -0
  29. package/lib/chart/events.d.ts +120 -0
  30. package/lib/chart/events.js +471 -0
  31. package/lib/chart/html.d.ts +21 -0
  32. package/lib/chart/html.js +154 -0
  33. package/lib/chart/runtime.d.ts +20 -0
  34. package/lib/chart/runtime.js +270 -0
  35. package/lib/chart/series.d.ts +102 -0
  36. package/lib/chart/series.js +424 -0
  37. package/lib/chart/source-token.d.ts +64 -0
  38. package/lib/chart/source-token.js +217 -0
  39. package/lib/chart/spec.d.ts +58 -0
  40. package/lib/chart/spec.js +265 -0
  41. package/lib/chart/tool.d.ts +86 -0
  42. package/lib/chart/tool.js +288 -0
  43. package/lib/chart/vendor/LICENSE +201 -0
  44. package/lib/chart/vendor/VERSION.json +8 -0
  45. package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
  46. package/lib/data-collector/args.d.ts +38 -0
  47. package/lib/data-collector/args.js +114 -0
  48. package/lib/data-collector/dataset-tools.d.ts +3 -0
  49. package/lib/data-collector/dataset-tools.js +421 -0
  50. package/lib/data-collector/describe.d.ts +99 -0
  51. package/lib/data-collector/describe.js +256 -0
  52. package/lib/data-collector/hub.d.ts +187 -0
  53. package/lib/data-collector/hub.js +469 -0
  54. package/lib/data-collector/query-time.d.ts +58 -0
  55. package/lib/data-collector/query-time.js +101 -0
  56. package/lib/data-collector/query.d.ts +57 -0
  57. package/lib/data-collector/query.js +473 -0
  58. package/lib/data-collector/store.d.ts +499 -0
  59. package/lib/data-collector/store.js +1486 -0
  60. package/lib/data-collector/time-axis.d.ts +169 -0
  61. package/lib/data-collector/time-axis.js +419 -0
  62. package/lib/data-collector/tools.d.ts +32 -0
  63. package/lib/data-collector/tools.js +216 -0
  64. package/lib/index.d.ts +275 -0
  65. package/lib/index.js +458 -0
  66. package/lib/net/eastmoney-client.d.ts +104 -0
  67. package/lib/net/eastmoney-client.js +118 -0
  68. package/lib/net/throttle.d.ts +16 -0
  69. package/lib/net/throttle.js +34 -0
  70. package/lib/ocr/client.d.ts +82 -0
  71. package/lib/ocr/client.js +281 -0
  72. package/lib/ocr/markdown.d.ts +75 -0
  73. package/lib/ocr/markdown.js +209 -0
  74. package/lib/ocr/store.d.ts +119 -0
  75. package/lib/ocr/store.js +226 -0
  76. package/lib/sources/eastmoney-http.d.ts +2 -0
  77. package/lib/sources/eastmoney-http.js +1253 -0
  78. package/lib/sources/fuyao-core.d.ts +52 -0
  79. package/lib/sources/fuyao-core.js +96 -0
  80. package/lib/sources/fuyao-rest.d.ts +14 -0
  81. package/lib/sources/fuyao-rest.js +2057 -0
  82. package/lib/sources/security-code.d.ts +27 -0
  83. package/lib/sources/security-code.js +104 -0
  84. package/lib/sources/security-identity.d.ts +24 -0
  85. package/lib/sources/security-identity.js +89 -0
  86. package/lib/sources/tencent-http.d.ts +2 -0
  87. package/lib/sources/tencent-http.js +1053 -0
  88. package/lib/sources/wind-mcp.d.ts +8 -0
  89. package/lib/sources/wind-mcp.js +593 -0
  90. package/lib/time/tools.d.ts +74 -0
  91. package/lib/time/tools.js +464 -0
  92. package/lib/tool-exec.d.ts +29 -0
  93. package/lib/tool-exec.js +17 -0
  94. package/lib/web-retriever/engines.d.ts +61 -0
  95. package/lib/web-retriever/engines.js +202 -0
  96. package/lib/web-retriever/html-markdown.d.ts +50 -0
  97. package/lib/web-retriever/html-markdown.js +321 -0
  98. package/lib/web-retriever/local-fetch.d.ts +121 -0
  99. package/lib/web-retriever/local-fetch.js +562 -0
  100. package/lib/web-retriever/retriever.d.ts +58 -0
  101. package/lib/web-retriever/retriever.js +94 -0
  102. package/lib/web-retriever/sources.d.ts +235 -0
  103. package/lib/web-retriever/sources.js +961 -0
  104. package/lib/web-retriever/tools.d.ts +50 -0
  105. package/lib/web-retriever/tools.js +1029 -0
  106. package/lib/web-retriever/wind-client.d.ts +69 -0
  107. package/lib/web-retriever/wind-client.js +311 -0
  108. package/locale/en.json +6 -0
  109. package/locale/zh.json +6 -0
  110. package/package.json +84 -4
  111. package/preset/capital-generation/README.md +161 -0
  112. package/preset/capital-generation/agent.patch.yml +509 -0
  113. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
  114. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
  115. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
  116. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
  117. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * 根 Agent 的工具收敛:把"只有子 Agent 该用"的工具从**主 Agent**的工具表里拿掉。
3
+ *
4
+ * 为什么需要它:preset 行注册在 standing composition 层,而主 Agent 与子 Agent 的
5
+ * scope 都是 standing 的**子节点(兄弟)**。因此:
6
+ * - 在 preset 层用 `tools.restrict({deny})` 会把子 Agent 一起 deny
7
+ * (`dsh-tools` 的 `view()` 里 allow/deny 只是交集过滤,永远不能把祖先 deny 掉的名字加回来),
8
+ * 连 `visualization_specialist` 的 `toolFilter.allow: [render_chart]` 都会失效;
9
+ * - 只在**根 Agent 自己的 scope** 上 deny,才既拿掉主 Agent 的入口、又不影响子 Agent。
10
+ *
11
+ * ⚠️ 监听必须注册在**未打 scope 标签的 context**(`ctx.root`)上。
12
+ * 实测(2026-09-17,dsh 0.1.5-rc.1):`agent/created` 的路由键是 **agent 对象本身**
13
+ * (`dsh-agent` 的 `agentCarrier(agent) = scopeTarget(agent, agent)`),不是 agent 的 scope key。
14
+ * `dsh-scope` 的准入规则只放行"未打标签的监听器"或"键在 carrier key 祖先链上"的监听器,
15
+ * 所以注册在 standing scope 上的监听器**收不到**这个事件——表现是主 Agent 的工具表照旧有
16
+ * `render_chart`,而且没有任何报错(静默失效)。宿主平面行之所以能用 `ctx.on('agent/created')`,
17
+ * 正是因为它们未打标签。因此这里取 `ctx.root`,再用 `agentPresets.composedPreset(agent.ctx)`
18
+ * 筛出本 preset 的 agent。
19
+ */
20
+ /**
21
+ * 出网工具名(唯一一份事实来源):检索面 2 + wind 2 + 具名来源查询面 9。
22
+ *
23
+ * ⚠️ **不含 `ocr`**:主 Agent 保留它的本地形态(`file` / `doc_id`),只由
24
+ * {@link rootOcrGuardReason} 关掉 `url` 形态——名字级 deny 做不到这个区分。
25
+ *
26
+ * 主 Agent 侧「外部检索只经 web_retriever 回传」必须是**结构件**,不能只写在 persona 里:
27
+ * 2026-09-24 review 复现——persona 禁直连清单当时写的是 `web_retriever_*` 通配,工具改名后
28
+ * 通配一个都不匹配,禁令名存实亡,而工具表里那 13 个入口始终摆着。逐名 deny 由本清单决定,
29
+ * preset 与测试都从这里取名字(`test/persona.test.mjs` 交叉核验),新增来源只改这一处。
30
+ */
31
+ export declare const RETRIEVAL_DENIED_TOOLS: readonly ["anysearch_search", "web_retriever_fetch", "wind_docs_announcements", "wind_docs_news", "cls_telegraph", "wscn_lives", "cninfo_irm", "sseinfo_qa", "eastmoney_724", "eastmoney_stock_news", "eastmoney_reports", "sina_reports", "ths_eps_forecast"];
32
+ /** 文档解析工具名(`web_retriever` 的第三个工作面)。 */
33
+ export declare const OCR_TOOL_NAME = "ocr";
34
+ /**
35
+ * 根 Agent 的 `ocr` 只放行**本地形态**(`file` / `doc_id`),`url` 形态拒绝。
36
+ *
37
+ * 为什么不整名 deny:用户把 PDF 放在工作目录用 `@xxx.pdf` 推给主 Agent 是正当入口,
38
+ * 解析它不需要经过委派;但 `url` 形态等于"主 Agent 自己决定去戳一个外部链接",
39
+ * 那是「外部检索只经 web_retriever」这条结构约束唯一被留的口子。名字级 deny 做不到
40
+ * 这个区分(`render_chart` 的教训一样:可见性不是权限),所以用调用级 guard。
41
+ */
42
+ export declare const OCR_ROOT_URL_DENIED: string;
43
+ /** 单次调用的判定:根 Agent 用 `ocr` 提交外部 `url` 才拒绝,其余一律放行。 */
44
+ export declare function rootOcrGuardReason(exec: GuardedExecution | undefined): string | undefined;
45
+ /**
46
+ * shell 工具名:preset 里这两行是**平台成对**挂载的(`agent.patch.yml` 的 `tool-bash` /
47
+ * `tool-pwsh`,靠 `process.platform === 'win32'` 互斥 `disabled`),所以名字由平台决定。
48
+ *
49
+ * ⛔ 但**闸门必须认全集**:只认本平台那一个,另一端就是静默放行——同一个能力两个入口,
50
+ * 本地测试只覆盖了接好的那个(AGENTS.md §9.7 的第三种形状)。根侧 deny 与
51
+ * `bash-guard.ts` 的子会话 guard 都从这里取名字,不再各写一份。
52
+ */
53
+ export declare const SHELL_TOOL_NAMES: readonly ["bash", "pwsh"];
54
+ export declare const ROOT_AGENT_DENIED_TOOLS: readonly ["render_chart", "subagent_visualization_specialist", "prepare_chart_source", "anysearch_search", "web_retriever_fetch", "wind_docs_announcements", "wind_docs_news", "cls_telegraph", "wscn_lives", "cninfo_irm", "sseinfo_qa", "eastmoney_724", "eastmoney_stock_news", "eastmoney_reports", "sina_reports", "ths_eps_forecast", "bash", "pwsh"];
55
+ /** 本 preset 的 id(`composedPreset` 的返回值)。 */
56
+ export declare const CAPITAL_PRESET_ID = "capital-generation";
57
+ interface ToolRestriction {
58
+ allow?: readonly string[];
59
+ deny?: readonly string[];
60
+ }
61
+ /**
62
+ * `ctx.tools` 里本仓真正用到的两个方法。
63
+ *
64
+ * `guard` 是单调执行闸门(`dsh-tools` 的 `ToolGuard`):同步检查一次调用,返回字符串即拒绝,
65
+ * **没有 allow 结果**,所以任何监听顺序都翻不回 allow。bash 闸门(`bash-guard.ts`)用它,
66
+ * 因为「只按调用内容 deny」这件事只有这里有正确的语义。
67
+ */
68
+ interface RestrictedToolRuntime {
69
+ restrict?(filter: ToolRestriction): unknown;
70
+ guard?(guard: (execution: GuardedExecution) => string | undefined): unknown;
71
+ }
72
+ /** 闸门看到的调用形状(`name` + 解析后的 `arguments` + 调用方 agent)。 */
73
+ export interface GuardedExecution {
74
+ readonly name?: string;
75
+ readonly arguments?: unknown;
76
+ readonly agent?: AgentLike;
77
+ }
78
+ /** 事件载荷里我们真正用到的 Agent 形状。 */
79
+ export interface AgentLike {
80
+ readonly session?: {
81
+ readonly id?: string;
82
+ readonly header?: {
83
+ readonly parentSession?: string;
84
+ };
85
+ };
86
+ readonly ctx?: {
87
+ readonly tools?: RestrictedToolRuntime;
88
+ readonly [key: string]: unknown;
89
+ };
90
+ }
91
+ /** 监听用的 context 形状(结构类型,便于单测注入假实现)。 */
92
+ export interface PolicyContext {
93
+ on?(event: string, listener: (payload: {
94
+ agent?: AgentLike;
95
+ }) => void): unknown;
96
+ effect?(callback: () => unknown, label?: string): unknown;
97
+ get?(name: string): unknown;
98
+ logger?: {
99
+ warn(message: string): void;
100
+ };
101
+ readonly root?: PolicyContext;
102
+ readonly [key: string]: unknown;
103
+ }
104
+ export type RootToolPolicyOutcome = 'restricted' | 'skipped' | 'unavailable';
105
+ /** 根 Agent 判据:没有 parentSession。子 Agent 一律有(spawn provider 写入)。 */
106
+ export declare function isRootAgent(agent: AgentLike | undefined): boolean;
107
+ /** 会话 id(guard 归档用);缺失或空串返回 undefined。 */
108
+ export declare function agentSessionId(agent: AgentLike | undefined): string | undefined;
109
+ /** 只有本 preset 的 agent 才收敛:别的 preset 的 agent 不该被我们碰。 */
110
+ export declare function isCapitalAgent(ctx: PolicyContext, agent: AgentLike | undefined): boolean;
111
+ /**
112
+ * 根收敛的**现场记录**(取证用,不改任何行为)。
113
+ *
114
+ * 为什么必须有:`registerRootToolPolicy` 有三条互不相同的静默出口——读不到 registry、
115
+ * `composedPreset` 判 false、逐名 `restrict()` 抛错被 catch 跳过(此时 outcome 仍记
116
+ * `restricted`)。三条都**不留痕迹**,而桌面端不落盘 `ctx.logger`,于是"收敛没生效"这件事
117
+ * 在桌面端等于不可观测。2026-09-30 桌面端实测:Capital 根会话的 `request.header.tools`
118
+ * 里 `pwsh` / `request_data` / `dc_status` / `render_chart` 全在,就是这条不可观测让它
119
+ * 一路只表现为"模型不太守纪律"。
120
+ *
121
+ * 读法:`dc_status` 的 `root_tool_policy` 字段(模型可见、可回传,不依赖宿主日志)。
122
+ */
123
+ export interface RootPolicyProbe {
124
+ /** 根 Agent 会话 id(子 Agent 也有 id,用来区分两条路径)。 */
125
+ agentId: string | undefined;
126
+ root: boolean;
127
+ /** `composedPreset` 的原样返回值;`'no-registry'` = registry 或方法不在,`'error'` = 它抛错。 */
128
+ presetId: string;
129
+ outcome: 'pending' | 'restricted' | 'unavailable' | 'skipped' | 'not-capital';
130
+ /** 真正被 `restrict({deny})` 接住的工具名。 */
131
+ denied: string[];
132
+ /** `restrict()` 抛错因而被静默跳过的名字——非空就说明收敛只成功了一部分。 */
133
+ failed: string[];
134
+ }
135
+ /** 最近 {@link MAX_PROBES} 条收敛现场(进程内;不落盘,随宿主进程生灭)。 */
136
+ export declare function rootPolicyProbes(): RootPolicyProbe[];
137
+ /** 只给测试用:清空现场记录,避免用例之间互相污染。 */
138
+ export declare function resetRootPolicyProbes(): void;
139
+ /**
140
+ * 只对根 Agent 生效地 deny {@link ROOT_AGENT_DENIED_TOOLS}。
141
+ *
142
+ * 逐名 restrict:`tools.restrict()` 对"本 scope 看不到的名字"会抛错(别的 preset、或行还没挂载),
143
+ * 逐名调用把这种"本来就不该有"的情况变成无害跳过,而不是整批失败。
144
+ * 任何异常都不抛出:调用方是 agent 创建路径,宁可少收敛也不能挡住建 agent。
145
+ */
146
+ export declare function restrictRootAgentTools(agent: AgentLike | undefined, record?: {
147
+ denied: string[];
148
+ failed: string[];
149
+ }): RootToolPolicyOutcome;
150
+ /**
151
+ * 在**根 Agent 自己的 scope** 上装 `ocr` 的调用级闸门(子 Agent 不装:`url` 形态归它们)。
152
+ *
153
+ * 与 `restrictRootAgentTools` 同一个监听、同一份判据;guard 不可用或安装异常都不影响
154
+ * 已经完成的 deny 收敛(宁可少一层闸门,不能挡住建 agent)。
155
+ */
156
+ export declare function installRootOcrGuard(agent: AgentLike | undefined): (() => void) | undefined;
157
+ /**
158
+ * 注册 `agent/created` 监听。监听器只做一件事:对本 preset 的根 Agent 收敛一次。
159
+ *
160
+ * 注册位置是 Root context(未打标签),因此能收到全部 agent;`agentPresets` 负责筛出本 preset。
161
+ * Root 监听器不随本 fiber 卸载,所以显式用 `ctx.effect` 持有它的 disposer。
162
+ */
163
+ export declare function registerRootToolPolicy(ctx: PolicyContext): void;
164
+ export {};
@@ -0,0 +1,309 @@
1
+ /**
2
+ * 根 Agent 的工具收敛:把"只有子 Agent 该用"的工具从**主 Agent**的工具表里拿掉。
3
+ *
4
+ * 为什么需要它:preset 行注册在 standing composition 层,而主 Agent 与子 Agent 的
5
+ * scope 都是 standing 的**子节点(兄弟)**。因此:
6
+ * - 在 preset 层用 `tools.restrict({deny})` 会把子 Agent 一起 deny
7
+ * (`dsh-tools` 的 `view()` 里 allow/deny 只是交集过滤,永远不能把祖先 deny 掉的名字加回来),
8
+ * 连 `visualization_specialist` 的 `toolFilter.allow: [render_chart]` 都会失效;
9
+ * - 只在**根 Agent 自己的 scope** 上 deny,才既拿掉主 Agent 的入口、又不影响子 Agent。
10
+ *
11
+ * ⚠️ 监听必须注册在**未打 scope 标签的 context**(`ctx.root`)上。
12
+ * 实测(2026-09-17,dsh 0.1.5-rc.1):`agent/created` 的路由键是 **agent 对象本身**
13
+ * (`dsh-agent` 的 `agentCarrier(agent) = scopeTarget(agent, agent)`),不是 agent 的 scope key。
14
+ * `dsh-scope` 的准入规则只放行"未打标签的监听器"或"键在 carrier key 祖先链上"的监听器,
15
+ * 所以注册在 standing scope 上的监听器**收不到**这个事件——表现是主 Agent 的工具表照旧有
16
+ * `render_chart`,而且没有任何报错(静默失效)。宿主平面行之所以能用 `ctx.on('agent/created')`,
17
+ * 正是因为它们未打标签。因此这里取 `ctx.root`,再用 `agentPresets.composedPreset(agent.ctx)`
18
+ * 筛出本 preset 的 agent。
19
+ */
20
+ /**
21
+ * 出网工具名(唯一一份事实来源):检索面 2 + wind 2 + 具名来源查询面 9。
22
+ *
23
+ * ⚠️ **不含 `ocr`**:主 Agent 保留它的本地形态(`file` / `doc_id`),只由
24
+ * {@link rootOcrGuardReason} 关掉 `url` 形态——名字级 deny 做不到这个区分。
25
+ *
26
+ * 主 Agent 侧「外部检索只经 web_retriever 回传」必须是**结构件**,不能只写在 persona 里:
27
+ * 2026-09-24 review 复现——persona 禁直连清单当时写的是 `web_retriever_*` 通配,工具改名后
28
+ * 通配一个都不匹配,禁令名存实亡,而工具表里那 13 个入口始终摆着。逐名 deny 由本清单决定,
29
+ * preset 与测试都从这里取名字(`test/persona.test.mjs` 交叉核验),新增来源只改这一处。
30
+ */
31
+ export const RETRIEVAL_DENIED_TOOLS = [
32
+ 'anysearch_search', 'web_retriever_fetch',
33
+ 'wind_docs_announcements', 'wind_docs_news',
34
+ 'cls_telegraph', 'wscn_lives', 'cninfo_irm', 'sseinfo_qa',
35
+ 'eastmoney_724', 'eastmoney_stock_news', 'eastmoney_reports', 'sina_reports', 'ths_eps_forecast',
36
+ ];
37
+ /** 文档解析工具名(`web_retriever` 的第三个工作面)。 */
38
+ export const OCR_TOOL_NAME = 'ocr';
39
+ /**
40
+ * 根 Agent 的 `ocr` 只放行**本地形态**(`file` / `doc_id`),`url` 形态拒绝。
41
+ *
42
+ * 为什么不整名 deny:用户把 PDF 放在工作目录用 `@xxx.pdf` 推给主 Agent 是正当入口,
43
+ * 解析它不需要经过委派;但 `url` 形态等于"主 Agent 自己决定去戳一个外部链接",
44
+ * 那是「外部检索只经 web_retriever」这条结构约束唯一被留的口子。名字级 deny 做不到
45
+ * 这个区分(`render_chart` 的教训一样:可见性不是权限),所以用调用级 guard。
46
+ */
47
+ export const OCR_ROOT_URL_DENIED = 'ocr 的 url 形态只对被委派的 web_retriever 开放:外部 PDF 链接请委派 subagent_web_retriever 解析。'
48
+ + '工作目录里的本地文档(file)与已解析产物(doc_id + pages/query)可以直接调用。';
49
+ /** guard 的调用形状里 `arguments` 是解析后的对象(不是 JSON 字符串)。 */
50
+ function isRecord(value) {
51
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
52
+ }
53
+ /** 单次调用的判定:根 Agent 用 `ocr` 提交外部 `url` 才拒绝,其余一律放行。 */
54
+ export function rootOcrGuardReason(exec) {
55
+ if (exec?.name !== OCR_TOOL_NAME)
56
+ return undefined;
57
+ const args = isRecord(exec.arguments) ? exec.arguments : undefined;
58
+ if (args !== undefined && typeof args.url === 'string' && args.url.length > 0)
59
+ return OCR_ROOT_URL_DENIED;
60
+ return undefined;
61
+ }
62
+ /**
63
+ * shell 工具名:preset 里这两行是**平台成对**挂载的(`agent.patch.yml` 的 `tool-bash` /
64
+ * `tool-pwsh`,靠 `process.platform === 'win32'` 互斥 `disabled`),所以名字由平台决定。
65
+ *
66
+ * ⛔ 但**闸门必须认全集**:只认本平台那一个,另一端就是静默放行——同一个能力两个入口,
67
+ * 本地测试只覆盖了接好的那个(AGENTS.md §9.7 的第三种形状)。根侧 deny 与
68
+ * `bash-guard.ts` 的子会话 guard 都从这里取名字,不再各写一份。
69
+ */
70
+ export const SHELL_TOOL_NAMES = ['bash', 'pwsh'];
71
+ export const ROOT_AGENT_DENIED_TOOLS = [
72
+ /** 出图只走 data_junior 的可视化 gate(main → junior → specialist)。 */
73
+ 'render_chart',
74
+ /** 孙 Agent 的创建工具只属于 data_junior:主 Agent 拿到它只会绕过 gate。 */
75
+ 'subagent_visualization_specialist',
76
+ /** 只签发给 delegated data agent(运行时本就拒绝根会话),留在表里只会诱导误调。 */
77
+ 'prepare_chart_source',
78
+ /**
79
+ * 十三个出网工具全部收敛:外部检索只有一个入口(web_retriever 子 Agent),它的兄弟行
80
+ * (data_collector / data_junior / visualization_specialist)的 allow 里本来就没有这些名字,
81
+ * 因此根侧 deny 不影响它们,只拿掉主 Agent 自己的直连能力。
82
+ * 唯一的例外是 `ocr`:它同时服务"解析外部 PDF 链接"与"解析用户放在工作目录的本地文档",
83
+ * 后者是主 Agent 的正当入口,所以不整名 deny,改由 {@link rootOcrGuardReason} 关掉 `url` 形态。
84
+ *
85
+ * 宿主自己挂的 `web_search` / `web_fetch` **已于 2026-09-30 从这份名单清掉**:双端实测
86
+ * (Linux 根会话 22 颗、桌面端 40 颗的工具表)都没有这两个名字,`restrict()` 对它们只会
87
+ * 抛错被跳过。留着不伤人,但代价是**每次现场记录都固定多两条 `failed`**,把"真有名字
88
+ * deny 不动"这种信号埋掉。它们仍由 persona 与 `capital-web-protocol` 的文案约束;
89
+ * **宿主若哪天把它们挂进根作用域,要重新加回这份名单**(`test/root-tool-policy.test.mjs`
90
+ * 钉的是"当前不在了",不是"永远不该在")。
91
+ */
92
+ ...RETRIEVAL_DENIED_TOOLS,
93
+ /**
94
+ * bash 只给 data_junior(它是数据侧的"纯计算兜底")。主 Agent 不需要它,而且一旦拥有
95
+ * 就等于拿到通用执行 + 整机读 + 出网能力,会绕过数据调度与出图两道结构 gate。
96
+ */
97
+ ...SHELL_TOOL_NAMES,
98
+ ];
99
+ /** 本 preset 的 id(`composedPreset` 的返回值)。 */
100
+ export const CAPITAL_PRESET_ID = 'capital-generation';
101
+ /** 根 Agent 判据:没有 parentSession。子 Agent 一律有(spawn provider 写入)。 */
102
+ export function isRootAgent(agent) {
103
+ if (agent?.session === undefined)
104
+ return false;
105
+ const parent = agent.session.header?.parentSession;
106
+ return typeof parent !== 'string' || parent.length === 0;
107
+ }
108
+ /** 会话 id(guard 归档用);缺失或空串返回 undefined。 */
109
+ export function agentSessionId(agent) {
110
+ const id = agent?.session?.id;
111
+ return typeof id === 'string' && id.length > 0 ? id : undefined;
112
+ }
113
+ /** 只有本 preset 的 agent 才收敛:别的 preset 的 agent 不该被我们碰。 */
114
+ export function isCapitalAgent(ctx, agent) {
115
+ let presets;
116
+ try {
117
+ presets = ctx.get?.('agentPresets');
118
+ }
119
+ catch {
120
+ presets = undefined;
121
+ }
122
+ if (presets === undefined || typeof presets.composedPreset !== 'function') {
123
+ // 读不到 registry 时保守放行:逐名 restrict 本身会拒绝未知工具名,不会误伤别的 preset。
124
+ return true;
125
+ }
126
+ try {
127
+ return presets.composedPreset(agent?.ctx) === CAPITAL_PRESET_ID;
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ const probes = [];
134
+ const MAX_PROBES = 8;
135
+ /** 最近 {@link MAX_PROBES} 条收敛现场(进程内;不落盘,随宿主进程生灭)。 */
136
+ export function rootPolicyProbes() {
137
+ return probes.map((probe) => ({ ...probe, denied: [...probe.denied], failed: [...probe.failed] }));
138
+ }
139
+ /** 只给测试用:清空现场记录,避免用例之间互相污染。 */
140
+ export function resetRootPolicyProbes() {
141
+ probes.length = 0;
142
+ }
143
+ /** `composedPreset` 的原样答案(诊断口径,不参与判定;判定仍只有一份在 {@link isCapitalAgent})。 */
144
+ function presetIdOf(ctx, agent) {
145
+ let presets;
146
+ try {
147
+ presets = ctx.get?.('agentPresets');
148
+ }
149
+ catch {
150
+ return 'no-registry';
151
+ }
152
+ if (presets === undefined || typeof presets.composedPreset !== 'function')
153
+ return 'no-registry';
154
+ try {
155
+ return presets.composedPreset(agent?.ctx) ?? 'undefined';
156
+ }
157
+ catch {
158
+ return 'error';
159
+ }
160
+ }
161
+ /**
162
+ * 只对根 Agent 生效地 deny {@link ROOT_AGENT_DENIED_TOOLS}。
163
+ *
164
+ * 逐名 restrict:`tools.restrict()` 对"本 scope 看不到的名字"会抛错(别的 preset、或行还没挂载),
165
+ * 逐名调用把这种"本来就不该有"的情况变成无害跳过,而不是整批失败。
166
+ * 任何异常都不抛出:调用方是 agent 创建路径,宁可少收敛也不能挡住建 agent。
167
+ */
168
+ export function restrictRootAgentTools(agent, record) {
169
+ if (!isRootAgent(agent))
170
+ return 'skipped';
171
+ let tools;
172
+ try {
173
+ tools = agent?.ctx?.tools;
174
+ }
175
+ catch {
176
+ return 'unavailable';
177
+ }
178
+ if (tools === undefined || typeof tools.restrict !== 'function')
179
+ return 'unavailable';
180
+ for (const name of ROOT_AGENT_DENIED_TOOLS) {
181
+ try {
182
+ tools.restrict({ deny: [name] });
183
+ record?.denied.push(name);
184
+ }
185
+ catch {
186
+ // 这个 scope 本来就没有这个名字(别的 preset / 该行未挂载):跳过即可。
187
+ record?.failed.push(name);
188
+ }
189
+ }
190
+ return 'restricted';
191
+ }
192
+ /**
193
+ * 在**根 Agent 自己的 scope** 上装 `ocr` 的调用级闸门(子 Agent 不装:`url` 形态归它们)。
194
+ *
195
+ * 与 `restrictRootAgentTools` 同一个监听、同一份判据;guard 不可用或安装异常都不影响
196
+ * 已经完成的 deny 收敛(宁可少一层闸门,不能挡住建 agent)。
197
+ */
198
+ export function installRootOcrGuard(agent) {
199
+ if (!isRootAgent(agent))
200
+ return undefined;
201
+ let tools;
202
+ try {
203
+ tools = agent?.ctx?.tools;
204
+ }
205
+ catch {
206
+ return undefined;
207
+ }
208
+ if (tools === undefined || typeof tools.guard !== 'function')
209
+ return undefined;
210
+ try {
211
+ const dispose = tools.guard(rootOcrGuardReason);
212
+ return typeof dispose === 'function' ? dispose : undefined;
213
+ }
214
+ catch {
215
+ return undefined;
216
+ }
217
+ }
218
+ /**
219
+ * 注册 `agent/created` 监听。监听器只做一件事:对本 preset 的根 Agent 收敛一次。
220
+ *
221
+ * 注册位置是 Root context(未打标签),因此能收到全部 agent;`agentPresets` 负责筛出本 preset。
222
+ * Root 监听器不随本 fiber 卸载,所以显式用 `ctx.effect` 持有它的 disposer。
223
+ */
224
+ export function registerRootToolPolicy(ctx) {
225
+ const listenCtx = ctx.root ?? ctx;
226
+ if (typeof listenCtx.on !== 'function')
227
+ return;
228
+ /** 根 Agent 会话 id → `ocr` guard 的 disposer;agent/disposed 时释放,避免跨实例残留。 */
229
+ const guards = new Map();
230
+ const install = () => {
231
+ const stopCreated = listenCtx.on?.('agent/created', (payload) => {
232
+ try {
233
+ const agent = payload?.agent;
234
+ const isRoot = isRootAgent(agent);
235
+ // 只记根 Agent 的现场:子 Agent 一律有 parentSession、本来就不该被收敛,
236
+ // 把它们也塞进这份上限 8 条的窗口里,会把唯一有意义的那行挤掉。
237
+ const probe = isRoot
238
+ ? { agentId: agentSessionId(agent), root: true, presetId: presetIdOf(ctx, agent), outcome: 'pending', denied: [], failed: [] }
239
+ : undefined;
240
+ if (probe !== undefined) {
241
+ probes.push(probe);
242
+ if (probes.length > MAX_PROBES)
243
+ probes.shift();
244
+ }
245
+ if (!isCapitalAgent(ctx, agent)) {
246
+ if (probe !== undefined)
247
+ probe.outcome = 'not-capital';
248
+ return;
249
+ }
250
+ const outcome = restrictRootAgentTools(agent, probe);
251
+ if (probe !== undefined)
252
+ probe.outcome = outcome;
253
+ if (outcome === 'unavailable') {
254
+ ctx.logger?.warn(`capital-generation: 根 Agent 工具收敛未生效(${outcome}):主 Agent 仍会看到 ${ROOT_AGENT_DENIED_TOOLS.join(' / ')}`);
255
+ }
256
+ const dispose = installRootOcrGuard(agent);
257
+ const id = agentSessionId(agent);
258
+ if (dispose !== undefined && id !== undefined)
259
+ guards.set(id, dispose);
260
+ }
261
+ catch (error) {
262
+ ctx.logger?.warn(`capital-generation: 根 Agent 工具收敛异常:${error instanceof Error ? error.message : String(error)}`);
263
+ }
264
+ });
265
+ const stopDisposed = listenCtx.on?.('agent/disposed', (payload) => {
266
+ try {
267
+ const id = agentSessionId(payload?.agent);
268
+ if (id === undefined)
269
+ return;
270
+ const dispose = guards.get(id);
271
+ if (dispose === undefined)
272
+ return;
273
+ guards.delete(id);
274
+ dispose();
275
+ }
276
+ catch { /* 释放失败不该影响 agent 拆除路径 */ }
277
+ });
278
+ return () => {
279
+ try {
280
+ if (typeof stopCreated === 'function')
281
+ stopCreated();
282
+ }
283
+ catch { /* ignore */ }
284
+ try {
285
+ if (typeof stopDisposed === 'function')
286
+ stopDisposed();
287
+ }
288
+ catch { /* ignore */ }
289
+ for (const dispose of guards.values()) {
290
+ try {
291
+ dispose();
292
+ }
293
+ catch { /* ignore */ }
294
+ }
295
+ guards.clear();
296
+ };
297
+ };
298
+ if (typeof ctx.effect === 'function') {
299
+ ctx.effect(() => {
300
+ const dispose = install();
301
+ return () => {
302
+ if (typeof dispose === 'function')
303
+ dispose();
304
+ };
305
+ }, 'capital-generation.root-tool-policy()');
306
+ return;
307
+ }
308
+ install();
309
+ }
@@ -0,0 +1,65 @@
1
+ import type { SessionLike } from '../data-collector/store.js';
2
+ /** Dataset/profile 的一跳 scope 规则;token 让 nested child 复用这个 owner scope。 */
3
+ export declare function chartSessionScopeId(session: SessionLike): string;
4
+ export interface ChartArtifactRef {
5
+ chart_ref: string;
6
+ chart_id: string;
7
+ task_id: string | null;
8
+ dataset_id: string | null;
9
+ source_label: string | null;
10
+ captured_at: number | null;
11
+ kind: string;
12
+ axis: 'time' | 'index';
13
+ chart_url: string | null;
14
+ spec_path: string;
15
+ series_path: string;
16
+ html_path: string;
17
+ created_at: number;
18
+ expires_at: number;
19
+ }
20
+ export interface ChartArtifactRegistryOptions {
21
+ now?: () => number;
22
+ retentionMs?: number;
23
+ newRef?: () => string;
24
+ }
25
+ /**
26
+ * In-process registry for chart_ref capabilities.
27
+ *
28
+ * The chart files live in the workspace, while this registry keeps the
29
+ * session/task binding that makes a chart safe to present to its own session
30
+ * (and to derive the `deliverables/presented` owner scope).
31
+ * Process restart invalidates chart_ref capabilities; chart.html remains the
32
+ * explicit offline fallback.
33
+ */
34
+ export declare class ChartArtifactRegistry {
35
+ private readonly records;
36
+ private readonly now;
37
+ private readonly retentionMs;
38
+ private readonly newRef;
39
+ constructor(options?: ChartArtifactRegistryOptions);
40
+ private purgeExpired;
41
+ issue(input: {
42
+ session: SessionLike;
43
+ chart_id: string;
44
+ task_id?: string | null;
45
+ dataset_id?: string | null;
46
+ source_label?: string | null;
47
+ captured_at?: number | null;
48
+ kind: string;
49
+ axis: 'time' | 'index';
50
+ chart_url: string | null;
51
+ spec_path: string;
52
+ series_path: string;
53
+ html_path: string;
54
+ created_at?: number;
55
+ expires_at?: number;
56
+ }): ChartArtifactRef;
57
+ resolve(input: {
58
+ session: SessionLike;
59
+ chart_ref: string;
60
+ task_id?: string | null;
61
+ }): ChartArtifactRef;
62
+ revoke(chartRef: string): boolean;
63
+ size(): number;
64
+ private publicRecord;
65
+ }
@@ -0,0 +1,107 @@
1
+ import { ChartError } from './errors.js';
2
+ const CHART_REF_PATTERN = /^chart_[A-Za-z0-9_-]{1,128}$/;
3
+ const DEFAULT_CHART_ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
4
+ function defaultChartRef() {
5
+ const cryptoLike = globalThis.crypto;
6
+ const suffix = cryptoLike?.randomUUID
7
+ ? cryptoLike.randomUUID()
8
+ : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
9
+ return `chart_${suffix}`;
10
+ }
11
+ /** Dataset/profile 的一跳 scope 规则;token 让 nested child 复用这个 owner scope。 */
12
+ export function chartSessionScopeId(session) {
13
+ const parent = session.header?.parentSession;
14
+ return typeof parent === 'string' && parent.length > 0 ? parent : session.id;
15
+ }
16
+ /**
17
+ * In-process registry for chart_ref capabilities.
18
+ *
19
+ * The chart files live in the workspace, while this registry keeps the
20
+ * session/task binding that makes a chart safe to present to its own session
21
+ * (and to derive the `deliverables/presented` owner scope).
22
+ * Process restart invalidates chart_ref capabilities; chart.html remains the
23
+ * explicit offline fallback.
24
+ */
25
+ export class ChartArtifactRegistry {
26
+ records = new Map();
27
+ now;
28
+ retentionMs;
29
+ newRef;
30
+ constructor(options = {}) {
31
+ this.now = options.now ?? (() => Date.now());
32
+ this.retentionMs = options.retentionMs ?? DEFAULT_CHART_ARTIFACT_RETENTION_MS;
33
+ this.newRef = options.newRef ?? defaultChartRef;
34
+ if (!Number.isSafeInteger(this.retentionMs) || this.retentionMs <= 0) {
35
+ throw new Error('ChartArtifactRegistry retentionMs must be a positive safe integer');
36
+ }
37
+ }
38
+ purgeExpired(now) {
39
+ for (const [chartRef, record] of this.records) {
40
+ if (record.expires_at <= now)
41
+ this.records.delete(chartRef);
42
+ }
43
+ }
44
+ issue(input) {
45
+ const now = input.created_at ?? this.now();
46
+ this.purgeExpired(now);
47
+ const chartRef = this.newRef();
48
+ if (!CHART_REF_PATTERN.test(chartRef) || this.records.has(chartRef)) {
49
+ throw new ChartError('chart_ref_invalid', 'generated chart_ref is invalid or already exists');
50
+ }
51
+ const expiresAt = Math.min(input.expires_at ?? now + this.retentionMs, now + this.retentionMs);
52
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= now) {
53
+ throw new ChartError('chart_ref_invalid', 'chart artifact expiration is invalid');
54
+ }
55
+ const record = {
56
+ chart_ref: chartRef,
57
+ chart_id: input.chart_id,
58
+ task_id: input.task_id ?? null,
59
+ dataset_id: input.dataset_id ?? null,
60
+ source_label: input.source_label ?? null,
61
+ captured_at: input.captured_at ?? null,
62
+ kind: input.kind,
63
+ axis: input.axis,
64
+ chart_url: input.chart_url,
65
+ spec_path: input.spec_path,
66
+ series_path: input.series_path,
67
+ html_path: input.html_path,
68
+ created_at: now,
69
+ expires_at: expiresAt,
70
+ owner_scope_id: chartSessionScopeId(input.session),
71
+ owner_cwd: input.session.header?.cwd ?? null,
72
+ };
73
+ this.records.set(chartRef, record);
74
+ return this.publicRecord(record);
75
+ }
76
+ resolve(input) {
77
+ if (typeof input.chart_ref !== 'string' || !CHART_REF_PATTERN.test(input.chart_ref)) {
78
+ throw new ChartError('chart_ref_invalid', 'chart_ref is invalid');
79
+ }
80
+ const now = this.now();
81
+ this.purgeExpired(now);
82
+ const record = this.records.get(input.chart_ref);
83
+ if (!record)
84
+ throw new ChartError('chart_ref_invalid', 'chart_ref is unknown or expired');
85
+ if (record.owner_scope_id !== chartSessionScopeId(input.session)) {
86
+ throw new ChartError('chart_ref_scope_mismatch', 'chart_ref does not belong to the current session scope');
87
+ }
88
+ if (record.owner_cwd !== (input.session.header?.cwd ?? null)) {
89
+ throw new ChartError('chart_ref_scope_mismatch', 'chart_ref does not belong to the current workspace');
90
+ }
91
+ if (input.task_id !== undefined && input.task_id !== null && record.task_id !== input.task_id) {
92
+ throw new ChartError('chart_ref_task_mismatch', 'chart_ref does not belong to the current task');
93
+ }
94
+ return this.publicRecord(record);
95
+ }
96
+ revoke(chartRef) {
97
+ return this.records.delete(chartRef);
98
+ }
99
+ size() {
100
+ this.purgeExpired(this.now());
101
+ return this.records.size;
102
+ }
103
+ publicRecord(record) {
104
+ const { owner_scope_id: _ownerScope, ...publicRecord } = record;
105
+ return publicRecord;
106
+ }
107
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Largest-Triangle-Three-Buckets 下采样:返回**保留下来的行下标**。
3
+ *
4
+ * 为什么返回下标而不是点:一张图上可能同时有主序列、K 线、成交量副图与标记点,
5
+ * 它们必须**共用同一组时间轴**。若各自下采样,同一时刻会在不同子图上错位。
6
+ * 于是只对"主序列"跑一次 LTTB 求下标,再把这组下标套用到所有子序列上。
7
+ */
8
+ export declare function largestTriangleThreeBuckets(values: readonly number[], threshold: number): number[];