psyclaw 0.24.0 → 0.25.2

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 (277) hide show
  1. package/README.md +46 -53
  2. package/{dist/apps → apps}/panel/index.html +28 -50
  3. package/package.json +10 -20
  4. package/scripts/__pycache__/build_dist.cpython-314.pyc +0 -0
  5. package/scripts/__pycache__/build_whitepaper_docx.cpython-314.pyc +0 -0
  6. package/scripts/build_preprint_docx.py +729 -0
  7. package/scripts/build_preprint_figures.py +238 -0
  8. package/scripts/build_three_line_table_fixture.py +56 -0
  9. package/scripts/build_whitepaper_docx.py +768 -0
  10. package/scripts/check-branding.mjs +159 -0
  11. package/scripts/copy-panel.mjs +11 -0
  12. package/scripts/rebrand-pi.mjs +292 -0
  13. package/scripts/verify_docx.py +41 -0
  14. package/skills/core/citation-audit/SKILL.md +9 -9
  15. package/skills/core/evidence-capture/SKILL.md +9 -9
  16. package/skills/core/manifest.json +15 -15
  17. package/skills/core/research-brief/SKILL.md +8 -8
  18. package/skills/core/research-intake/SKILL.md +9 -9
  19. package/dist/src/adapters/pi/extension.d.ts +0 -19
  20. package/dist/src/adapters/pi/extension.js +0 -735
  21. package/dist/src/adapters/pi/extension.js.map +0 -1
  22. package/dist/src/adapters/pi/model.d.ts +0 -72
  23. package/dist/src/adapters/pi/model.js +0 -152
  24. package/dist/src/adapters/pi/model.js.map +0 -1
  25. package/dist/src/adapters/pi/resources.d.ts +0 -23
  26. package/dist/src/adapters/pi/resources.js +0 -54
  27. package/dist/src/adapters/pi/resources.js.map +0 -1
  28. package/dist/src/adapters/pi/rpc.d.ts +0 -56
  29. package/dist/src/adapters/pi/rpc.js +0 -248
  30. package/dist/src/adapters/pi/rpc.js.map +0 -1
  31. package/dist/src/adapters/pi/session.d.ts +0 -30
  32. package/dist/src/adapters/pi/session.js +0 -64
  33. package/dist/src/adapters/pi/session.js.map +0 -1
  34. package/dist/src/agents/catalog.d.ts +0 -30
  35. package/dist/src/agents/catalog.js +0 -118
  36. package/dist/src/agents/catalog.js.map +0 -1
  37. package/dist/src/agents/discover.d.ts +0 -28
  38. package/dist/src/agents/discover.js +0 -127
  39. package/dist/src/agents/discover.js.map +0 -1
  40. package/dist/src/agents/import.d.ts +0 -67
  41. package/dist/src/agents/import.js +0 -801
  42. package/dist/src/agents/import.js.map +0 -1
  43. package/dist/src/analysis/hooks.d.ts +0 -81
  44. package/dist/src/analysis/hooks.js +0 -139
  45. package/dist/src/analysis/hooks.js.map +0 -1
  46. package/dist/src/analysis/pre-registration.d.ts +0 -18
  47. package/dist/src/analysis/pre-registration.js +0 -86
  48. package/dist/src/analysis/pre-registration.js.map +0 -1
  49. package/dist/src/branding.d.ts +0 -43
  50. package/dist/src/branding.js +0 -127
  51. package/dist/src/branding.js.map +0 -1
  52. package/dist/src/chat.d.ts +0 -16
  53. package/dist/src/chat.js +0 -67
  54. package/dist/src/chat.js.map +0 -1
  55. package/dist/src/cli.d.ts +0 -2
  56. package/dist/src/cli.js +0 -316
  57. package/dist/src/cli.js.map +0 -1
  58. package/dist/src/core/citations.d.ts +0 -44
  59. package/dist/src/core/citations.js +0 -48
  60. package/dist/src/core/citations.js.map +0 -1
  61. package/dist/src/core/contracts.d.ts +0 -119
  62. package/dist/src/core/contracts.js +0 -2
  63. package/dist/src/core/contracts.js.map +0 -1
  64. package/dist/src/core/docx.d.ts +0 -11
  65. package/dist/src/core/docx.js +0 -46
  66. package/dist/src/core/docx.js.map +0 -1
  67. package/dist/src/core/doi.d.ts +0 -41
  68. package/dist/src/core/doi.js +0 -82
  69. package/dist/src/core/doi.js.map +0 -1
  70. package/dist/src/core/evidence-policy.d.ts +0 -24
  71. package/dist/src/core/evidence-policy.js +0 -308
  72. package/dist/src/core/evidence-policy.js.map +0 -1
  73. package/dist/src/core/filekind.d.ts +0 -15
  74. package/dist/src/core/filekind.js +0 -82
  75. package/dist/src/core/filekind.js.map +0 -1
  76. package/dist/src/core/hash.d.ts +0 -2
  77. package/dist/src/core/hash.js +0 -17
  78. package/dist/src/core/hash.js.map +0 -1
  79. package/dist/src/core/pricing.d.ts +0 -38
  80. package/dist/src/core/pricing.js +0 -110
  81. package/dist/src/core/pricing.js.map +0 -1
  82. package/dist/src/core/redact.d.ts +0 -12
  83. package/dist/src/core/redact.js +0 -48
  84. package/dist/src/core/redact.js.map +0 -1
  85. package/dist/src/core/references.d.ts +0 -55
  86. package/dist/src/core/references.js +0 -102
  87. package/dist/src/core/references.js.map +0 -1
  88. package/dist/src/core/schemas.d.ts +0 -162
  89. package/dist/src/core/schemas.js +0 -160
  90. package/dist/src/core/schemas.js.map +0 -1
  91. package/dist/src/extension.d.ts +0 -1
  92. package/dist/src/extension.js +0 -2
  93. package/dist/src/extension.js.map +0 -1
  94. package/dist/src/index.d.ts +0 -72
  95. package/dist/src/index.js +0 -73
  96. package/dist/src/index.js.map +0 -1
  97. package/dist/src/install/installer.d.ts +0 -147
  98. package/dist/src/install/installer.js +0 -743
  99. package/dist/src/install/installer.js.map +0 -1
  100. package/dist/src/integrations/contracts.d.ts +0 -35
  101. package/dist/src/integrations/contracts.js +0 -27
  102. package/dist/src/integrations/contracts.js.map +0 -1
  103. package/dist/src/integrations/mcp.d.ts +0 -155
  104. package/dist/src/integrations/mcp.js +0 -826
  105. package/dist/src/integrations/mcp.js.map +0 -1
  106. package/dist/src/knowledge-map/contracts.d.ts +0 -20
  107. package/dist/src/knowledge-map/contracts.js +0 -2
  108. package/dist/src/knowledge-map/contracts.js.map +0 -1
  109. package/dist/src/knowledge-map/index.d.ts +0 -4
  110. package/dist/src/knowledge-map/index.js +0 -39
  111. package/dist/src/knowledge-map/index.js.map +0 -1
  112. package/dist/src/literature/contracts.d.ts +0 -106
  113. package/dist/src/literature/contracts.js +0 -2
  114. package/dist/src/literature/contracts.js.map +0 -1
  115. package/dist/src/literature/fulltext.d.ts +0 -7
  116. package/dist/src/literature/fulltext.js +0 -24
  117. package/dist/src/literature/fulltext.js.map +0 -1
  118. package/dist/src/literature/knowledge-map.d.ts +0 -2
  119. package/dist/src/literature/knowledge-map.js +0 -20
  120. package/dist/src/literature/knowledge-map.js.map +0 -1
  121. package/dist/src/literature/ledger.d.ts +0 -3
  122. package/dist/src/literature/ledger.js +0 -17
  123. package/dist/src/literature/ledger.js.map +0 -1
  124. package/dist/src/literature/metadata.d.ts +0 -6
  125. package/dist/src/literature/metadata.js +0 -32
  126. package/dist/src/literature/metadata.js.map +0 -1
  127. package/dist/src/memory/store.d.ts +0 -22
  128. package/dist/src/memory/store.js +0 -73
  129. package/dist/src/memory/store.js.map +0 -1
  130. package/dist/src/orchestration/contracts.d.ts +0 -61
  131. package/dist/src/orchestration/contracts.js +0 -2
  132. package/dist/src/orchestration/contracts.js.map +0 -1
  133. package/dist/src/orchestration/pi-executor.d.ts +0 -28
  134. package/dist/src/orchestration/pi-executor.js +0 -169
  135. package/dist/src/orchestration/pi-executor.js.map +0 -1
  136. package/dist/src/orchestration/runner.d.ts +0 -196
  137. package/dist/src/orchestration/runner.js +0 -991
  138. package/dist/src/orchestration/runner.js.map +0 -1
  139. package/dist/src/orchestration/scheduler.d.ts +0 -8
  140. package/dist/src/orchestration/scheduler.js +0 -335
  141. package/dist/src/orchestration/scheduler.js.map +0 -1
  142. package/dist/src/panel/events.d.ts +0 -20
  143. package/dist/src/panel/events.js +0 -93
  144. package/dist/src/panel/events.js.map +0 -1
  145. package/dist/src/panel/extension.d.ts +0 -8
  146. package/dist/src/panel/extension.js +0 -114
  147. package/dist/src/panel/extension.js.map +0 -1
  148. package/dist/src/panel/literature-map.d.ts +0 -26
  149. package/dist/src/panel/literature-map.js +0 -34
  150. package/dist/src/panel/literature-map.js.map +0 -1
  151. package/dist/src/panel/projection.d.ts +0 -51
  152. package/dist/src/panel/projection.js +0 -288
  153. package/dist/src/panel/projection.js.map +0 -1
  154. package/dist/src/panel/server.d.ts +0 -46
  155. package/dist/src/panel/server.js +0 -1610
  156. package/dist/src/panel/server.js.map +0 -1
  157. package/dist/src/panel/usage.d.ts +0 -53
  158. package/dist/src/panel/usage.js +0 -150
  159. package/dist/src/panel/usage.js.map +0 -1
  160. package/dist/src/project/bootstrap.d.ts +0 -10
  161. package/dist/src/project/bootstrap.js +0 -83
  162. package/dist/src/project/bootstrap.js.map +0 -1
  163. package/dist/src/project/hitl.d.ts +0 -12
  164. package/dist/src/project/hitl.js +0 -43
  165. package/dist/src/project/hitl.js.map +0 -1
  166. package/dist/src/project/jsonl.d.ts +0 -16
  167. package/dist/src/project/jsonl.js +0 -93
  168. package/dist/src/project/jsonl.js.map +0 -1
  169. package/dist/src/project/manuscript.d.ts +0 -21
  170. package/dist/src/project/manuscript.js +0 -74
  171. package/dist/src/project/manuscript.js.map +0 -1
  172. package/dist/src/project/paths.d.ts +0 -38
  173. package/dist/src/project/paths.js +0 -166
  174. package/dist/src/project/paths.js.map +0 -1
  175. package/dist/src/project/versions.d.ts +0 -10
  176. package/dist/src/project/versions.js +0 -14
  177. package/dist/src/project/versions.js.map +0 -1
  178. package/dist/src/psyclaw-theme.d.ts +0 -88
  179. package/dist/src/psyclaw-theme.js +0 -89
  180. package/dist/src/psyclaw-theme.js.map +0 -1
  181. package/dist/src/research/brief.d.ts +0 -11
  182. package/dist/src/research/brief.js +0 -81
  183. package/dist/src/research/brief.js.map +0 -1
  184. package/dist/src/research/ledger.d.ts +0 -12
  185. package/dist/src/research/ledger.js +0 -43
  186. package/dist/src/research/ledger.js.map +0 -1
  187. package/dist/src/run.d.ts +0 -20
  188. package/dist/src/run.js +0 -34
  189. package/dist/src/run.js.map +0 -1
  190. package/dist/src/setup.d.ts +0 -54
  191. package/dist/src/setup.js +0 -221
  192. package/dist/src/setup.js.map +0 -1
  193. package/dist/src/skills/contracts.d.ts +0 -105
  194. package/dist/src/skills/contracts.js +0 -3
  195. package/dist/src/skills/contracts.js.map +0 -1
  196. package/dist/src/skills/preflight.d.ts +0 -10
  197. package/dist/src/skills/preflight.js +0 -39
  198. package/dist/src/skills/preflight.js.map +0 -1
  199. package/dist/src/skills/registry.d.ts +0 -28
  200. package/dist/src/skills/registry.js +0 -556
  201. package/dist/src/skills/registry.js.map +0 -1
  202. package/dist/src/style/artifact.d.ts +0 -6
  203. package/dist/src/style/artifact.js +0 -61
  204. package/dist/src/style/artifact.js.map +0 -1
  205. package/dist/src/style/cli-ui.d.ts +0 -41
  206. package/dist/src/style/cli-ui.js +0 -119
  207. package/dist/src/style/cli-ui.js.map +0 -1
  208. package/dist/src/style/contracts.d.ts +0 -34
  209. package/dist/src/style/contracts.js +0 -2
  210. package/dist/src/style/contracts.js.map +0 -1
  211. package/dist/src/style/registry.d.ts +0 -16
  212. package/dist/src/style/registry.js +0 -50
  213. package/dist/src/style/registry.js.map +0 -1
  214. package/dist/src/trust/manifest.d.ts +0 -42
  215. package/dist/src/trust/manifest.js +0 -53
  216. package/dist/src/trust/manifest.js.map +0 -1
  217. package/dist/src/trust/sbom.d.ts +0 -54
  218. package/dist/src/trust/sbom.js +0 -77
  219. package/dist/src/trust/sbom.js.map +0 -1
  220. package/dist/src/tui/app.d.ts +0 -3
  221. package/dist/src/tui/app.js +0 -103
  222. package/dist/src/tui/app.js.map +0 -1
  223. package/dist/src/tui/model.d.ts +0 -13
  224. package/dist/src/tui/model.js +0 -25
  225. package/dist/src/tui/model.js.map +0 -1
  226. package/dist/src/updates/check.d.ts +0 -43
  227. package/dist/src/updates/check.js +0 -68
  228. package/dist/src/updates/check.js.map +0 -1
  229. package/dist/src/updates/manifest.d.ts +0 -19
  230. package/dist/src/updates/manifest.js +0 -58
  231. package/dist/src/updates/manifest.js.map +0 -1
  232. package/dist/src/updates/registry.d.ts +0 -21
  233. package/dist/src/updates/registry.js +0 -82
  234. package/dist/src/updates/registry.js.map +0 -1
  235. package/dist/src/updates/skills.d.ts +0 -15
  236. package/dist/src/updates/skills.js +0 -39
  237. package/dist/src/updates/skills.js.map +0 -1
  238. package/dist/src/updates/status.d.ts +0 -7
  239. package/dist/src/updates/status.js +0 -37
  240. package/dist/src/updates/status.js.map +0 -1
  241. package/dist/src/updates/update.d.ts +0 -43
  242. package/dist/src/updates/update.js +0 -126
  243. package/dist/src/updates/update.js.map +0 -1
  244. package/dist/src/wizard.d.ts +0 -7
  245. package/dist/src/wizard.js +0 -90
  246. package/dist/src/wizard.js.map +0 -1
  247. package/dist/src/workflows/analysis-delegation.d.ts +0 -3
  248. package/dist/src/workflows/analysis-delegation.js +0 -96
  249. package/dist/src/workflows/analysis-delegation.js.map +0 -1
  250. package/dist/src/workflows/expert-review-runner.d.ts +0 -29
  251. package/dist/src/workflows/expert-review-runner.js +0 -35
  252. package/dist/src/workflows/expert-review-runner.js.map +0 -1
  253. package/dist/src/workflows/expert-review.d.ts +0 -140
  254. package/dist/src/workflows/expert-review.js +0 -111
  255. package/dist/src/workflows/expert-review.js.map +0 -1
  256. package/dist/src/workflows/institutional-fulltext.d.ts +0 -16
  257. package/dist/src/workflows/institutional-fulltext.js +0 -110
  258. package/dist/src/workflows/institutional-fulltext.js.map +0 -1
  259. package/dist/src/workflows/literature-review.d.ts +0 -3
  260. package/dist/src/workflows/literature-review.js +0 -76
  261. package/dist/src/workflows/literature-review.js.map +0 -1
  262. package/dist/src/workflows/meta-analysis.d.ts +0 -38
  263. package/dist/src/workflows/meta-analysis.js +0 -185
  264. package/dist/src/workflows/meta-analysis.js.map +0 -1
  265. package/dist/src/workflows/publish.d.ts +0 -51
  266. package/dist/src/workflows/publish.js +0 -153
  267. package/dist/src/workflows/publish.js.map +0 -1
  268. package/dist/src/workflows/spec.d.ts +0 -51
  269. package/dist/src/workflows/spec.js +0 -157
  270. package/dist/src/workflows/spec.js.map +0 -1
  271. package/dist/src/workflows/style-contract.d.ts +0 -24
  272. package/dist/src/workflows/style-contract.js +0 -23
  273. package/dist/src/workflows/style-contract.js.map +0 -1
  274. package/dist/src/workflows/writing-review.d.ts +0 -15
  275. package/dist/src/workflows/writing-review.js +0 -82
  276. package/dist/src/workflows/writing-review.js.map +0 -1
  277. package/package.json.psyclaw +0 -21
@@ -1,826 +0,0 @@
1
- import { spawn as nodeSpawn } from "node:child_process";
2
- import { createInterface } from "node:readline";
3
- import { sha256Text } from "../core/hash.js";
4
- import { PSYCLAW_VERSION } from "../branding.js";
5
- import { denyByDefault } from "./contracts.js";
6
- /**
7
- * The secure default: argv (never a shell) and an explicit env. The child
8
- * inherits nothing from `process.env`, so a provider secret in the parent
9
- * cannot leak into a tool subprocess unless the caller names it explicitly.
10
- */
11
- const defaultSpawn = (command, args, options) => {
12
- const child = nodeSpawn(command, args, {
13
- env: options.env ?? {},
14
- stdio: ["pipe", "pipe", "pipe"],
15
- shell: false,
16
- windowsHide: true,
17
- });
18
- return {
19
- stdout: child.stdout,
20
- stderr: child.stderr,
21
- stdin: { write: (chunk) => child.stdin.write(chunk), end: () => child.stdin.end() },
22
- kill: () => child.kill(),
23
- onClose: (handler) => child.on("close", handler),
24
- };
25
- };
26
- /** Line-delimited JSON-RPC transport over a spawned child. */
27
- export class StdioMcpTransport {
28
- child;
29
- handlers = [];
30
- closeHandlers = [];
31
- closed = false;
32
- constructor(config, spawn = defaultSpawn) {
33
- this.child = spawn(config.command, config.args, { env: config.env ?? {} });
34
- createInterface({ input: this.child.stdout, crlfDelay: Infinity }).on("line", (line) => {
35
- const trimmed = line.trim();
36
- if (trimmed.length === 0)
37
- return;
38
- let parsed;
39
- try {
40
- parsed = JSON.parse(trimmed);
41
- }
42
- catch {
43
- // Malformed server output is ignored. The request will fail by timeout
44
- // and the timeout path terminates this transport.
45
- return;
46
- }
47
- for (const handler of this.handlers)
48
- handler(parsed);
49
- });
50
- // stderr is drained for diagnostics but never logged verbatim; a server
51
- // must not be able to inject log text that the host then persists.
52
- createInterface({ input: this.child.stderr, crlfDelay: Infinity }).on("line", () => undefined);
53
- this.child.onClose(() => this.markClosed());
54
- }
55
- send(message) {
56
- if (this.closed)
57
- throw new McpTransportClosedError();
58
- this.child.stdin.write(`${JSON.stringify(message)}\n`);
59
- }
60
- onMessage(handler) {
61
- this.handlers.push(handler);
62
- }
63
- onClose(handler) {
64
- if (this.closed) {
65
- handler();
66
- return;
67
- }
68
- this.closeHandlers.push(handler);
69
- }
70
- close() {
71
- if (this.closed)
72
- return;
73
- this.closed = true;
74
- try {
75
- this.child.stdin.end();
76
- }
77
- catch {
78
- // The child may have already closed its pipe.
79
- }
80
- try {
81
- this.child.kill();
82
- }
83
- catch {
84
- // A failed kill is still represented as a closed transport.
85
- }
86
- this.notifyClosed();
87
- }
88
- markClosed() {
89
- if (this.closed)
90
- return;
91
- this.closed = true;
92
- this.notifyClosed();
93
- }
94
- notifyClosed() {
95
- const handlers = this.closeHandlers.splice(0);
96
- for (const handler of handlers)
97
- handler();
98
- }
99
- }
100
- export class McpTimeoutError extends Error {
101
- method;
102
- code = "mcp.timeout";
103
- constructor(method) {
104
- super("MCP request timed out");
105
- this.method = method;
106
- this.name = "McpTimeoutError";
107
- }
108
- }
109
- export class McpTransportClosedError extends Error {
110
- code = "mcp.transport-closed";
111
- constructor() {
112
- super("MCP transport closed");
113
- this.name = "McpTransportClosedError";
114
- }
115
- }
116
- class McpRemoteError extends Error {
117
- code = "mcp.remote-error";
118
- constructor() {
119
- // Never copy the server's error text. It can contain credentials or data.
120
- super("MCP server returned an error");
121
- this.name = "McpRemoteError";
122
- }
123
- }
124
- /** Minimal JSON-RPC client for the MCP initialize/list/call sequence. */
125
- export class McpClient {
126
- transport;
127
- nextId = 1;
128
- pending = new Map();
129
- closed = false;
130
- constructor(transport) {
131
- this.transport = transport;
132
- transport.onMessage((message) => this.handle(message));
133
- transport.onClose?.(() => {
134
- this.closed = true;
135
- this.rejectPending(new McpTransportClosedError());
136
- });
137
- }
138
- initialize(clientInfo, timeoutMs = 10000) {
139
- return this.request("initialize", {
140
- protocolVersion: "2024-11-05",
141
- capabilities: {},
142
- clientInfo,
143
- }, timeoutMs);
144
- }
145
- initialized() {
146
- this.transport.send({ jsonrpc: "2.0", method: "notifications/initialized" });
147
- }
148
- listTools(timeoutMs = 10000) {
149
- return this.request("tools/list", undefined, timeoutMs).then((result) => {
150
- const tools = result?.tools;
151
- if (!Array.isArray(tools))
152
- return [];
153
- // A malformed descriptor makes the discovery set ambiguous. Do not
154
- // silently drop it and expose a partially trusted server.
155
- if (tools.some((tool) => !isMcpTool(tool)))
156
- throw new Error("invalid MCP tool descriptor");
157
- return tools;
158
- });
159
- }
160
- callTool(name, args, timeoutMs = 30000) {
161
- return this.request("tools/call", { name, arguments: args }, timeoutMs);
162
- }
163
- close() {
164
- if (this.closed)
165
- return;
166
- this.closed = true;
167
- try {
168
- this.transport.close();
169
- }
170
- catch {
171
- // Treat an already-dead transport as closed.
172
- }
173
- this.rejectPending(new McpTransportClosedError());
174
- }
175
- request(method, params, timeoutMs) {
176
- if (this.closed)
177
- return Promise.reject(new McpTransportClosedError());
178
- const timeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.floor(timeoutMs) : 10000;
179
- const id = this.nextId;
180
- this.nextId += 1;
181
- return new Promise((resolve, reject) => {
182
- const timer = setTimeout(() => {
183
- const entry = this.pending.get(id);
184
- if (!entry)
185
- return;
186
- this.pending.delete(id);
187
- clearTimeout(entry.timer);
188
- // A timed-out side effect is indeterminate. Closing the transport is
189
- // mandatory so a late response/child cannot be mistaken for a retry.
190
- try {
191
- this.transport.close();
192
- }
193
- catch {
194
- // The request is still rejected below; a broken close implementation
195
- // must not leave the caller hanging.
196
- }
197
- reject(new McpTimeoutError(method));
198
- }, timeout);
199
- this.pending.set(id, { resolve, reject, timer });
200
- try {
201
- this.transport.send({
202
- jsonrpc: "2.0",
203
- id,
204
- method,
205
- ...(params === undefined ? {} : { params }),
206
- });
207
- }
208
- catch {
209
- const entry = this.pending.get(id);
210
- if (entry) {
211
- this.pending.delete(id);
212
- clearTimeout(entry.timer);
213
- }
214
- reject(new McpTransportClosedError());
215
- }
216
- });
217
- }
218
- rejectPending(error) {
219
- const entries = [...this.pending.values()];
220
- this.pending.clear();
221
- for (const entry of entries) {
222
- clearTimeout(entry.timer);
223
- entry.reject(error);
224
- }
225
- }
226
- handle(message) {
227
- if (!isRecord(message))
228
- return;
229
- const id = message.id;
230
- if (typeof id !== "number" || !this.pending.has(id))
231
- return;
232
- const entry = this.pending.get(id);
233
- if (!entry)
234
- return;
235
- this.pending.delete(id);
236
- clearTimeout(entry.timer);
237
- if (message.error !== undefined && message.error !== null) {
238
- entry.reject(new McpRemoteError());
239
- return;
240
- }
241
- entry.resolve(message.result);
242
- }
243
- }
244
- /** A real stdio MCP integration behind the shared `Integration` contract. */
245
- export class StdioMcpIntegration {
246
- config;
247
- spawn;
248
- transport;
249
- client;
250
- initialized = false;
251
- discoveryAttempted = false;
252
- discovered = new Map();
253
- /** Reservations survive a failed/timeout call to prevent blind replay. */
254
- idempotency = new Map();
255
- defaultEffect;
256
- constructor(config, spawn = defaultSpawn) {
257
- this.config = config;
258
- this.spawn = spawn;
259
- // Omitted host policy is read-only. A caller must opt into network/write
260
- // effects in the host config and still provide approval/idempotency.
261
- this.defaultEffect = isEffect(config.effect) ? config.effect : "read";
262
- }
263
- async list() {
264
- if (!this.config.enabled || !this.config.trusted)
265
- return [];
266
- if (this.discoveryAttempted)
267
- return this.descriptorSnapshot();
268
- try {
269
- const client = await this.connectAndInitialize();
270
- const tools = await client.listTools(this.timeoutFor("list"));
271
- const seen = new Set();
272
- const next = new Map();
273
- for (const tool of tools) {
274
- if (!isSafeToolName(tool.name) || seen.has(tool.name)) {
275
- // Duplicate/invalid names make the discovery set ambiguous. Do not
276
- // expose a partially trusted set.
277
- this.resetConnection(true);
278
- return [];
279
- }
280
- seen.add(tool.name);
281
- const policy = resolvePolicy(this.config, tool.name, this.defaultEffect);
282
- const effect = policy?.effect ?? this.defaultEffect;
283
- const allowed = policy !== undefined && policy.enabled && effectAllowed(this.config, effect);
284
- const resolved = policy === undefined
285
- ? undefined
286
- : {
287
- // Keep explicit host enablement separate from the effect
288
- // ceiling. Invocation can then report the precise blocked
289
- // reason instead of conflating it with an unknown tool.
290
- enabled: policy.enabled,
291
- effect,
292
- ...(policy.inputSchema !== undefined ? { inputSchema: policy.inputSchema } : {}),
293
- approvalRequired: policy.approvalRequired === true,
294
- };
295
- const descriptor = {
296
- id: `${this.config.id}.${tool.name}`,
297
- description: typeof tool.description === "string" ? tool.description.slice(0, 1000) : "",
298
- // This effect is always host-assigned. Any server-provided effect
299
- // field is intentionally not represented in McpTool.
300
- effect,
301
- source: this.config.id,
302
- trust: "approved",
303
- enabled: allowed,
304
- };
305
- next.set(tool.name, {
306
- name: tool.name,
307
- schema: tool.inputSchema,
308
- ...(resolved === undefined ? {} : { policy: resolved }),
309
- descriptor,
310
- });
311
- }
312
- this.discovered.clear();
313
- for (const [name, cached] of next)
314
- this.discovered.set(name, cached);
315
- this.discoveryAttempted = true;
316
- return this.descriptorSnapshot();
317
- }
318
- catch {
319
- this.resetConnection(true);
320
- return [];
321
- }
322
- }
323
- async health() {
324
- if (!this.config.enabled)
325
- return { ok: false, reason: "server disabled" };
326
- if (!this.config.trusted)
327
- return { ok: false, reason: "server not trusted" };
328
- try {
329
- await this.connectAndInitialize();
330
- return { ok: true };
331
- }
332
- catch (error) {
333
- this.resetConnection(true);
334
- return { ok: false, reason: failureCode(error) };
335
- }
336
- }
337
- async invoke(call, approval) {
338
- const fallback = this.defaultEffect;
339
- const safeCall = sanitizeCall(call);
340
- if (!isValidCallEnvelope(call)) {
341
- return denyByDefault(safeCall, fallback, "mcp.invalid-call", "mcp.invalid-call");
342
- }
343
- if (!this.config.enabled || !this.config.trusted) {
344
- return denyByDefault(safeCall, fallback, "mcp.untrusted", "mcp.untrusted");
345
- }
346
- if (!isValidApproval(approval)) {
347
- return denyByDefault(safeCall, fallback, "mcp.invalid-approval", "mcp.invalid-approval");
348
- }
349
- if (!this.discoveryAttempted) {
350
- return denyByDefault(safeCall, fallback, "mcp.tool-not-discovered", "mcp.tool-not-discovered");
351
- }
352
- const parsed = parseToolId(call.tool, this.config.id);
353
- if (parsed === undefined) {
354
- return denyByDefault(safeCall, fallback, "mcp.source-mismatch", "mcp.source-mismatch");
355
- }
356
- const cached = this.discovered.get(parsed);
357
- if (cached === undefined) {
358
- return denyByDefault(safeCall, fallback, "mcp.tool-not-discovered", "mcp.tool-not-discovered");
359
- }
360
- const policy = cached.policy;
361
- if (policy === undefined || !policy.enabled || cached.descriptor.source !== this.config.id) {
362
- return denyByDefault(safeCall, cached.descriptor.effect, "mcp.tool-not-enabled", "mcp.tool-not-enabled");
363
- }
364
- const requested = call.effect;
365
- if (requested !== undefined && requested !== policy.effect) {
366
- return denyByDefault(safeCall, policy.effect, "mcp.effect-mismatch", "mcp.effect-mismatch");
367
- }
368
- const requestedSource = call.source;
369
- if (requestedSource !== undefined && requestedSource !== this.config.id) {
370
- return denyByDefault(safeCall, policy.effect, "mcp.source-mismatch", "mcp.source-mismatch");
371
- }
372
- if (!effectAllowed(this.config, policy.effect)) {
373
- return denyByDefault(safeCall, policy.effect, "mcp.effect-not-allowed", "mcp.effect-not-allowed");
374
- }
375
- if (approval.decision === "denied") {
376
- return denyByDefault(safeCall, policy.effect, "mcp.approval-required", "mcp.approval-required");
377
- }
378
- if ((policy.effect !== "read" || policy.approvalRequired) && approval.decision !== "approved") {
379
- return denyByDefault(safeCall, policy.effect, "mcp.approval-required", "mcp.approval-required");
380
- }
381
- const schema = policy.inputSchema ?? cached.schema;
382
- if (!validateToolInput(schema, call.input)) {
383
- return denyByDefault(safeCall, policy.effect, "mcp.invalid-input", "mcp.invalid-input");
384
- }
385
- const suppliedIdempotencyKey = validIdempotencyKey(call.idempotencyKey) ? call.idempotencyKey : undefined;
386
- let idempotencyKey = suppliedIdempotencyKey;
387
- if (policy.effect !== "read") {
388
- if (idempotencyKey === undefined) {
389
- return denyByDefault(safeCall, policy.effect, "mcp.idempotency-required", "mcp.idempotency-required");
390
- }
391
- let inputCanonical;
392
- try {
393
- inputCanonical = canonicalJson(call.input);
394
- }
395
- catch {
396
- inputCanonical = undefined;
397
- }
398
- if (inputCanonical === undefined) {
399
- return denyByDefault(safeCall, policy.effect, "mcp.invalid-input", "mcp.invalid-input");
400
- }
401
- const fingerprint = sha256Text(`${call.tool}\n${policy.effect}\n${inputCanonical}`);
402
- const previous = this.idempotency.get(idempotencyKey);
403
- if (previous !== undefined) {
404
- return denyByDefault(safeCall, policy.effect, previous === fingerprint ? "mcp.duplicate-idempotency" : "mcp.idempotency-conflict", previous === fingerprint ? "mcp.duplicate-idempotency" : "mcp.idempotency-conflict");
405
- }
406
- // Reserve before crossing the transport boundary. The reservation is
407
- // intentionally retained after errors and timeouts.
408
- this.idempotency.set(idempotencyKey, fingerprint);
409
- }
410
- try {
411
- const client = await this.connectAndInitialize();
412
- const result = await client.callTool(parsed, call.input, this.timeoutFor("call"));
413
- const now = new Date().toISOString();
414
- const resultText = canonicalJson(result) ?? "<invalid-result>";
415
- return {
416
- schemaVersion: "psyclaw/tool-receipt/v1",
417
- runId: safeCall.runId,
418
- taskId: safeCall.taskId,
419
- tool: safeCall.tool,
420
- effect: policy.effect,
421
- approval: approval.decision,
422
- ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
423
- ok: true,
424
- resultHash: sha256Text(resultText),
425
- startedAt: now,
426
- finishedAt: now,
427
- };
428
- }
429
- catch (error) {
430
- // A disconnect/timeout/error invalidates the discovery session. Never
431
- // retry automatically: a side effect may have happened remotely.
432
- this.resetConnection(true);
433
- return denyByDefault(safeCall, policy.effect, failureCode(error), failureCode(error));
434
- }
435
- }
436
- close() {
437
- this.resetConnection(true);
438
- }
439
- descriptorSnapshot() {
440
- return [...this.discovered.values()].map((entry) => ({ ...entry.descriptor }));
441
- }
442
- timeoutFor(kind) {
443
- const configured = this.config.requestTimeoutMs;
444
- if (Number.isFinite(configured) && configured !== undefined && configured > 0)
445
- return Math.floor(configured);
446
- return kind === "list" ? 10000 : 30000;
447
- }
448
- async connectAndInitialize() {
449
- if (this.client === undefined) {
450
- this.transport = new StdioMcpTransport(this.config, this.spawn);
451
- this.client = new McpClient(this.transport);
452
- this.initialized = false;
453
- }
454
- if (!this.initialized) {
455
- await this.client.initialize({ name: "psyclaw", version: PSYCLAW_VERSION }, this.timeoutFor("list"));
456
- this.client.initialized();
457
- this.initialized = true;
458
- }
459
- return this.client;
460
- }
461
- resetConnection(invalidateDiscovery) {
462
- try {
463
- this.client?.close();
464
- }
465
- catch {
466
- // The boundary remains invalidated even if a custom transport throws.
467
- }
468
- this.client = undefined;
469
- this.transport = undefined;
470
- this.initialized = false;
471
- if (invalidateDiscovery) {
472
- this.discoveryAttempted = false;
473
- this.discovered.clear();
474
- }
475
- }
476
- }
477
- /**
478
- * Deliberately lazy MCP boundary retained for M1/M2 compatibility. Discovery
479
- * is data-only; invocation is denied until `StdioMcpIntegration` is approved.
480
- */
481
- export class LazyMcpIntegration {
482
- config;
483
- constructor(config) {
484
- this.config = config;
485
- }
486
- async list() {
487
- if (!this.config.enabled || !this.config.trusted)
488
- return [];
489
- return [];
490
- }
491
- async health() {
492
- if (!this.config.enabled)
493
- return { ok: false, reason: "server disabled" };
494
- if (!this.config.trusted)
495
- return { ok: false, reason: "server not trusted" };
496
- return { ok: false, reason: "stdio adapter not enabled in M1" };
497
- }
498
- async invoke(call, approval) {
499
- const approved = approval.decision === "approved";
500
- return denyByDefault(sanitizeCall(call), "network", approved ? "mcp.transport-disabled" : "mcp.approval-required", approved ? "mcp.transport-disabled" : "mcp.approval-required");
501
- }
502
- }
503
- const EFFECTS = ["read", "write", "network", "destructive"];
504
- function isEffect(value) {
505
- return typeof value === "string" && EFFECTS.includes(value);
506
- }
507
- function isRecord(value) {
508
- return typeof value === "object" && value !== null && !Array.isArray(value);
509
- }
510
- function isMcpTool(value) {
511
- if (!isRecord(value) || typeof value.name !== "string")
512
- return false;
513
- if (value.description !== undefined && typeof value.description !== "string")
514
- return false;
515
- return true;
516
- }
517
- function isSafeToolName(value) {
518
- return value.length > 0 && value.length <= 256 && !/[\u0000-\u001f\u007f]/.test(value);
519
- }
520
- function parseToolId(tool, serverId) {
521
- if (!isSafeToolName(tool))
522
- return undefined;
523
- const prefix = `${serverId}.`;
524
- if (!tool.startsWith(prefix))
525
- return undefined;
526
- const name = tool.slice(prefix.length);
527
- return isSafeToolName(name) ? name : undefined;
528
- }
529
- function validIdempotencyKey(value) {
530
- return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(value);
531
- }
532
- function isValidApproval(value) {
533
- if (!isRecord(value))
534
- return false;
535
- const decision = value.decision;
536
- const actor = value.actor;
537
- const reason = value.reason;
538
- return (decision === "not-needed" || decision === "approved" || decision === "denied") &&
539
- (actor === "policy" || actor === "researcher") &&
540
- typeof reason === "string" && reason.length <= 2000;
541
- }
542
- function isValidCallEnvelope(value) {
543
- return isRecord(value) &&
544
- typeof value.runId === "string" && value.runId.length > 0 && value.runId.length <= 256 &&
545
- typeof value.taskId === "string" && value.taskId.length > 0 && value.taskId.length <= 256 &&
546
- typeof value.tool === "string";
547
- }
548
- function sanitizeCall(call) {
549
- const record = isRecord(call) ? call : {};
550
- const rawTool = typeof record.tool === "string" ? record.tool : "";
551
- const tool = isSafeToolName(rawTool) ? rawTool : "mcp.invalid-tool";
552
- const idempotencyKey = validIdempotencyKey(record.idempotencyKey) ? record.idempotencyKey : undefined;
553
- return {
554
- runId: typeof record.runId === "string" && record.runId.length <= 256 ? record.runId : "mcp.invalid-run",
555
- taskId: typeof record.taskId === "string" && record.taskId.length <= 256 ? record.taskId : "mcp.invalid-task",
556
- tool,
557
- input: record.input,
558
- ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
559
- };
560
- }
561
- function resolvePolicy(config, name, defaultEffect) {
562
- const canonical = `${config.id}.${name}`;
563
- const maps = [
564
- config.toolPolicies,
565
- config.toolPolicy,
566
- isPolicyMap(config.tools) ? config.tools : undefined,
567
- isPolicyMap(config.allowlist) ? config.allowlist : undefined,
568
- ];
569
- for (const map of maps) {
570
- if (map === undefined)
571
- continue;
572
- const entry = map[name] ?? map[canonical];
573
- if (entry === undefined)
574
- continue;
575
- if (typeof entry === "boolean") {
576
- return { enabled: entry, effect: defaultEffect, approvalRequired: false };
577
- }
578
- if (!isRecord(entry))
579
- return undefined;
580
- const enabled = entry.enabled === true;
581
- const effect = isEffect(entry.effect) ? entry.effect : defaultEffect;
582
- const inputSchema = Object.prototype.hasOwnProperty.call(entry, "inputSchema") ? entry.inputSchema : undefined;
583
- return {
584
- enabled,
585
- effect,
586
- ...(inputSchema === undefined ? {} : { inputSchema }),
587
- approvalRequired: entry.approvalRequired === true,
588
- };
589
- }
590
- const explicitLists = [
591
- config.enabledTools,
592
- config.toolAllowlist,
593
- config.allowedTools,
594
- Array.isArray(config.tools) ? config.tools : undefined,
595
- Array.isArray(config.allowlist) ? config.allowlist : undefined,
596
- ];
597
- const hasList = explicitLists.some((list) => list !== undefined);
598
- if (hasList) {
599
- const enabled = explicitLists.some((list) => list?.some((entry) => entry === name || entry === canonical));
600
- return enabled ? { enabled: true, effect: defaultEffect, approvalRequired: false } : undefined;
601
- }
602
- if (config.allowAllDiscoveredTools === true && defaultEffect === "read" && effectAllowed(config, "read")) {
603
- return { enabled: true, effect: defaultEffect, approvalRequired: false };
604
- }
605
- return undefined;
606
- }
607
- function isPolicyMap(value) {
608
- return isRecord(value);
609
- }
610
- function effectAllowed(config, effect) {
611
- if (!isEffect(effect))
612
- return false;
613
- if (config.allowedEffects !== undefined) {
614
- const configured = config.allowedEffects instanceof Set
615
- ? [...config.allowedEffects]
616
- : config.allowedEffects;
617
- if (!Array.isArray(configured) || configured.some((item) => !isEffect(item)))
618
- return false;
619
- return configured.includes(effect);
620
- }
621
- if (config.effectCeiling !== undefined) {
622
- const ceiling = config.effectCeiling;
623
- if (Array.isArray(ceiling)) {
624
- return ceiling.length > 0 && ceiling.every((item) => isEffect(item)) && ceiling.includes(effect);
625
- }
626
- if (!isEffect(ceiling))
627
- return false;
628
- const allowed = ceiling === "read"
629
- ? ["read"]
630
- : ceiling === "write"
631
- ? ["read", "write"]
632
- : ceiling === "network"
633
- ? ["read", "network"]
634
- : EFFECTS;
635
- return allowed.includes(effect);
636
- }
637
- return true;
638
- }
639
- function failureCode(error) {
640
- if (error instanceof McpTimeoutError)
641
- return error.code;
642
- if (error instanceof McpTransportClosedError)
643
- return error.code;
644
- if (error instanceof McpRemoteError)
645
- return error.code;
646
- return "mcp.call-failed";
647
- }
648
- function canonicalJson(value, seen = new Set(), depth = 0) {
649
- if (depth > 40)
650
- return undefined;
651
- if (value === null)
652
- return "null";
653
- if (typeof value === "string")
654
- return JSON.stringify(value);
655
- if (typeof value === "boolean")
656
- return value ? "true" : "false";
657
- if (typeof value === "number")
658
- return Number.isFinite(value) ? JSON.stringify(value) : undefined;
659
- if (typeof value !== "object")
660
- return undefined;
661
- if (seen.has(value))
662
- return undefined;
663
- seen.add(value);
664
- let result;
665
- if (Array.isArray(value)) {
666
- const parts = [];
667
- for (const item of value) {
668
- const encoded = canonicalJson(item, seen, depth + 1);
669
- if (encoded === undefined)
670
- return undefined;
671
- parts.push(encoded);
672
- }
673
- result = `[${parts.join(",")}]`;
674
- }
675
- else {
676
- const record = value;
677
- const parts = [];
678
- for (const key of Object.keys(record).sort()) {
679
- const encoded = canonicalJson(record[key], seen, depth + 1);
680
- if (encoded === undefined)
681
- return undefined;
682
- parts.push(`${JSON.stringify(key)}:${encoded}`);
683
- }
684
- result = `{${parts.join(",")}}`;
685
- }
686
- seen.delete(value);
687
- return result;
688
- }
689
- function validateToolInput(schema, input) {
690
- try {
691
- if (canonicalJson(input) === undefined)
692
- return false;
693
- const schemaText = canonicalJson(schema);
694
- if (schemaText === undefined || schemaText.length > 65_536)
695
- return false;
696
- return validateSchema(schema, input, 0, new Set());
697
- }
698
- catch {
699
- // Accessors/proxies and cyclic values are untrusted caller input.
700
- return false;
701
- }
702
- }
703
- /** A bounded JSON-Schema subset sufficient for MCP tool arguments. */
704
- function validateSchema(schema, value, depth, seen) {
705
- if (!isRecord(schema) || depth > 32)
706
- return false;
707
- if (seen.has(schema))
708
- return false;
709
- seen.add(schema);
710
- try {
711
- for (const keyword of ["allOf", "anyOf", "oneOf"]) {
712
- if (schema[keyword] !== undefined) {
713
- if (!Array.isArray(schema[keyword]) || schema[keyword].some((item) => !isRecord(item)))
714
- return false;
715
- const matches = schema[keyword].filter((item) => validateSchema(item, value, depth + 1, seen)).length;
716
- if (keyword === "allOf" && matches !== schema[keyword].length)
717
- return false;
718
- if (keyword === "anyOf" && matches < 1)
719
- return false;
720
- if (keyword === "oneOf" && matches !== 1)
721
- return false;
722
- }
723
- }
724
- if (schema.enum !== undefined) {
725
- if (!Array.isArray(schema.enum) || !schema.enum.some((entry) => canonicalJson(entry) === canonicalJson(value)))
726
- return false;
727
- }
728
- if (Object.prototype.hasOwnProperty.call(schema, "const") && canonicalJson(schema.const) !== canonicalJson(value))
729
- return false;
730
- if (schema.type !== undefined) {
731
- const types = Array.isArray(schema.type) ? schema.type : [schema.type];
732
- if (!types.every((item) => typeof item === "string"))
733
- return false;
734
- if (!types.some((type) => typeMatches(type, value)))
735
- return false;
736
- }
737
- if (typeof value === "string") {
738
- if (schema.minLength !== undefined && (!isFiniteInteger(schema.minLength) || value.length < schema.minLength))
739
- return false;
740
- if (schema.maxLength !== undefined && (!isFiniteInteger(schema.maxLength) || value.length > schema.maxLength))
741
- return false;
742
- if (schema.pattern !== undefined) {
743
- if (typeof schema.pattern !== "string")
744
- return false;
745
- if (schema.pattern.length > 512)
746
- return false;
747
- try {
748
- if (!new RegExp(schema.pattern).test(value))
749
- return false;
750
- }
751
- catch {
752
- return false;
753
- }
754
- }
755
- }
756
- if (typeof value === "number") {
757
- if (schema.minimum !== undefined && (typeof schema.minimum !== "number" || value < schema.minimum))
758
- return false;
759
- if (schema.maximum !== undefined && (typeof schema.maximum !== "number" || value > schema.maximum))
760
- return false;
761
- }
762
- if (Array.isArray(value)) {
763
- if (schema.minItems !== undefined && (!isFiniteInteger(schema.minItems) || value.length < schema.minItems))
764
- return false;
765
- if (schema.maxItems !== undefined && (!isFiniteInteger(schema.maxItems) || value.length > schema.maxItems))
766
- return false;
767
- if (schema.items !== undefined) {
768
- if (!isRecord(schema.items) || !value.every((item) => validateSchema(schema.items, item, depth + 1, seen)))
769
- return false;
770
- }
771
- }
772
- if (isRecord(value)) {
773
- if (schema.required !== undefined) {
774
- if (!Array.isArray(schema.required) || schema.required.some((item) => typeof item !== "string"))
775
- return false;
776
- if (schema.required.some((key) => !Object.prototype.hasOwnProperty.call(value, key)))
777
- return false;
778
- }
779
- let properties = {};
780
- if (schema.properties !== undefined) {
781
- if (!isRecord(schema.properties))
782
- return false;
783
- if (Object.keys(schema.properties).length > 256)
784
- return false;
785
- properties = schema.properties;
786
- for (const [key, child] of Object.entries(properties)) {
787
- if (Object.prototype.hasOwnProperty.call(value, key) && !validateSchema(child, value[key], depth + 1, seen))
788
- return false;
789
- }
790
- }
791
- if (schema.additionalProperties === false) {
792
- if (Object.keys(value).some((key) => !Object.prototype.hasOwnProperty.call(properties, key)))
793
- return false;
794
- }
795
- else if (isRecord(schema.additionalProperties)) {
796
- for (const key of Object.keys(value)) {
797
- if (!Object.prototype.hasOwnProperty.call(properties, key) && !validateSchema(schema.additionalProperties, value[key], depth + 1, seen))
798
- return false;
799
- }
800
- }
801
- else if (schema.additionalProperties !== undefined && schema.additionalProperties !== true) {
802
- return false;
803
- }
804
- }
805
- return true;
806
- }
807
- finally {
808
- seen.delete(schema);
809
- }
810
- }
811
- function typeMatches(type, value) {
812
- switch (type) {
813
- case "null": return value === null;
814
- case "boolean": return typeof value === "boolean";
815
- case "object": return isRecord(value);
816
- case "array": return Array.isArray(value);
817
- case "string": return typeof value === "string";
818
- case "number": return typeof value === "number" && Number.isFinite(value);
819
- case "integer": return typeof value === "number" && Number.isInteger(value);
820
- default: return false;
821
- }
822
- }
823
- function isFiniteInteger(value) {
824
- return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 1_000_000;
825
- }
826
- //# sourceMappingURL=mcp.js.map