psyclaw 0.24.0 → 0.25.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.
- package/README.md +46 -53
- package/{dist/apps → apps}/panel/index.html +28 -50
- package/package.json +10 -20
- package/scripts/__pycache__/build_dist.cpython-314.pyc +0 -0
- package/scripts/__pycache__/build_whitepaper_docx.cpython-314.pyc +0 -0
- package/scripts/build_preprint_docx.py +729 -0
- package/scripts/build_preprint_figures.py +238 -0
- package/scripts/build_three_line_table_fixture.py +56 -0
- package/scripts/build_whitepaper_docx.py +768 -0
- package/scripts/check-branding.mjs +159 -0
- package/scripts/copy-panel.mjs +11 -0
- package/scripts/rebrand-pi.mjs +291 -0
- package/scripts/verify_docx.py +41 -0
- package/skills/core/citation-audit/SKILL.md +9 -9
- package/skills/core/evidence-capture/SKILL.md +9 -9
- package/skills/core/manifest.json +15 -15
- package/skills/core/research-brief/SKILL.md +8 -8
- package/skills/core/research-intake/SKILL.md +9 -9
- package/dist/src/adapters/pi/extension.d.ts +0 -19
- package/dist/src/adapters/pi/extension.js +0 -735
- package/dist/src/adapters/pi/extension.js.map +0 -1
- package/dist/src/adapters/pi/model.d.ts +0 -72
- package/dist/src/adapters/pi/model.js +0 -152
- package/dist/src/adapters/pi/model.js.map +0 -1
- package/dist/src/adapters/pi/resources.d.ts +0 -23
- package/dist/src/adapters/pi/resources.js +0 -54
- package/dist/src/adapters/pi/resources.js.map +0 -1
- package/dist/src/adapters/pi/rpc.d.ts +0 -56
- package/dist/src/adapters/pi/rpc.js +0 -248
- package/dist/src/adapters/pi/rpc.js.map +0 -1
- package/dist/src/adapters/pi/session.d.ts +0 -30
- package/dist/src/adapters/pi/session.js +0 -64
- package/dist/src/adapters/pi/session.js.map +0 -1
- package/dist/src/agents/catalog.d.ts +0 -30
- package/dist/src/agents/catalog.js +0 -118
- package/dist/src/agents/catalog.js.map +0 -1
- package/dist/src/agents/discover.d.ts +0 -28
- package/dist/src/agents/discover.js +0 -127
- package/dist/src/agents/discover.js.map +0 -1
- package/dist/src/agents/import.d.ts +0 -67
- package/dist/src/agents/import.js +0 -801
- package/dist/src/agents/import.js.map +0 -1
- package/dist/src/analysis/hooks.d.ts +0 -81
- package/dist/src/analysis/hooks.js +0 -139
- package/dist/src/analysis/hooks.js.map +0 -1
- package/dist/src/analysis/pre-registration.d.ts +0 -18
- package/dist/src/analysis/pre-registration.js +0 -86
- package/dist/src/analysis/pre-registration.js.map +0 -1
- package/dist/src/branding.d.ts +0 -43
- package/dist/src/branding.js +0 -127
- package/dist/src/branding.js.map +0 -1
- package/dist/src/chat.d.ts +0 -16
- package/dist/src/chat.js +0 -67
- package/dist/src/chat.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.js +0 -316
- package/dist/src/cli.js.map +0 -1
- package/dist/src/core/citations.d.ts +0 -44
- package/dist/src/core/citations.js +0 -48
- package/dist/src/core/citations.js.map +0 -1
- package/dist/src/core/contracts.d.ts +0 -119
- package/dist/src/core/contracts.js +0 -2
- package/dist/src/core/contracts.js.map +0 -1
- package/dist/src/core/docx.d.ts +0 -11
- package/dist/src/core/docx.js +0 -46
- package/dist/src/core/docx.js.map +0 -1
- package/dist/src/core/doi.d.ts +0 -41
- package/dist/src/core/doi.js +0 -82
- package/dist/src/core/doi.js.map +0 -1
- package/dist/src/core/evidence-policy.d.ts +0 -24
- package/dist/src/core/evidence-policy.js +0 -308
- package/dist/src/core/evidence-policy.js.map +0 -1
- package/dist/src/core/filekind.d.ts +0 -15
- package/dist/src/core/filekind.js +0 -82
- package/dist/src/core/filekind.js.map +0 -1
- package/dist/src/core/hash.d.ts +0 -2
- package/dist/src/core/hash.js +0 -17
- package/dist/src/core/hash.js.map +0 -1
- package/dist/src/core/pricing.d.ts +0 -38
- package/dist/src/core/pricing.js +0 -110
- package/dist/src/core/pricing.js.map +0 -1
- package/dist/src/core/redact.d.ts +0 -12
- package/dist/src/core/redact.js +0 -48
- package/dist/src/core/redact.js.map +0 -1
- package/dist/src/core/references.d.ts +0 -55
- package/dist/src/core/references.js +0 -102
- package/dist/src/core/references.js.map +0 -1
- package/dist/src/core/schemas.d.ts +0 -162
- package/dist/src/core/schemas.js +0 -160
- package/dist/src/core/schemas.js.map +0 -1
- package/dist/src/extension.d.ts +0 -1
- package/dist/src/extension.js +0 -2
- package/dist/src/extension.js.map +0 -1
- package/dist/src/index.d.ts +0 -72
- package/dist/src/index.js +0 -73
- package/dist/src/index.js.map +0 -1
- package/dist/src/install/installer.d.ts +0 -147
- package/dist/src/install/installer.js +0 -743
- package/dist/src/install/installer.js.map +0 -1
- package/dist/src/integrations/contracts.d.ts +0 -35
- package/dist/src/integrations/contracts.js +0 -27
- package/dist/src/integrations/contracts.js.map +0 -1
- package/dist/src/integrations/mcp.d.ts +0 -155
- package/dist/src/integrations/mcp.js +0 -826
- package/dist/src/integrations/mcp.js.map +0 -1
- package/dist/src/knowledge-map/contracts.d.ts +0 -20
- package/dist/src/knowledge-map/contracts.js +0 -2
- package/dist/src/knowledge-map/contracts.js.map +0 -1
- package/dist/src/knowledge-map/index.d.ts +0 -4
- package/dist/src/knowledge-map/index.js +0 -39
- package/dist/src/knowledge-map/index.js.map +0 -1
- package/dist/src/literature/contracts.d.ts +0 -106
- package/dist/src/literature/contracts.js +0 -2
- package/dist/src/literature/contracts.js.map +0 -1
- package/dist/src/literature/fulltext.d.ts +0 -7
- package/dist/src/literature/fulltext.js +0 -24
- package/dist/src/literature/fulltext.js.map +0 -1
- package/dist/src/literature/knowledge-map.d.ts +0 -2
- package/dist/src/literature/knowledge-map.js +0 -20
- package/dist/src/literature/knowledge-map.js.map +0 -1
- package/dist/src/literature/ledger.d.ts +0 -3
- package/dist/src/literature/ledger.js +0 -17
- package/dist/src/literature/ledger.js.map +0 -1
- package/dist/src/literature/metadata.d.ts +0 -6
- package/dist/src/literature/metadata.js +0 -32
- package/dist/src/literature/metadata.js.map +0 -1
- package/dist/src/memory/store.d.ts +0 -22
- package/dist/src/memory/store.js +0 -73
- package/dist/src/memory/store.js.map +0 -1
- package/dist/src/orchestration/contracts.d.ts +0 -61
- package/dist/src/orchestration/contracts.js +0 -2
- package/dist/src/orchestration/contracts.js.map +0 -1
- package/dist/src/orchestration/pi-executor.d.ts +0 -28
- package/dist/src/orchestration/pi-executor.js +0 -169
- package/dist/src/orchestration/pi-executor.js.map +0 -1
- package/dist/src/orchestration/runner.d.ts +0 -196
- package/dist/src/orchestration/runner.js +0 -991
- package/dist/src/orchestration/runner.js.map +0 -1
- package/dist/src/orchestration/scheduler.d.ts +0 -8
- package/dist/src/orchestration/scheduler.js +0 -335
- package/dist/src/orchestration/scheduler.js.map +0 -1
- package/dist/src/panel/events.d.ts +0 -20
- package/dist/src/panel/events.js +0 -93
- package/dist/src/panel/events.js.map +0 -1
- package/dist/src/panel/extension.d.ts +0 -8
- package/dist/src/panel/extension.js +0 -114
- package/dist/src/panel/extension.js.map +0 -1
- package/dist/src/panel/literature-map.d.ts +0 -26
- package/dist/src/panel/literature-map.js +0 -34
- package/dist/src/panel/literature-map.js.map +0 -1
- package/dist/src/panel/projection.d.ts +0 -51
- package/dist/src/panel/projection.js +0 -288
- package/dist/src/panel/projection.js.map +0 -1
- package/dist/src/panel/server.d.ts +0 -46
- package/dist/src/panel/server.js +0 -1610
- package/dist/src/panel/server.js.map +0 -1
- package/dist/src/panel/usage.d.ts +0 -53
- package/dist/src/panel/usage.js +0 -150
- package/dist/src/panel/usage.js.map +0 -1
- package/dist/src/project/bootstrap.d.ts +0 -10
- package/dist/src/project/bootstrap.js +0 -83
- package/dist/src/project/bootstrap.js.map +0 -1
- package/dist/src/project/hitl.d.ts +0 -12
- package/dist/src/project/hitl.js +0 -43
- package/dist/src/project/hitl.js.map +0 -1
- package/dist/src/project/jsonl.d.ts +0 -16
- package/dist/src/project/jsonl.js +0 -93
- package/dist/src/project/jsonl.js.map +0 -1
- package/dist/src/project/manuscript.d.ts +0 -21
- package/dist/src/project/manuscript.js +0 -74
- package/dist/src/project/manuscript.js.map +0 -1
- package/dist/src/project/paths.d.ts +0 -38
- package/dist/src/project/paths.js +0 -166
- package/dist/src/project/paths.js.map +0 -1
- package/dist/src/project/versions.d.ts +0 -10
- package/dist/src/project/versions.js +0 -14
- package/dist/src/project/versions.js.map +0 -1
- package/dist/src/psyclaw-theme.d.ts +0 -88
- package/dist/src/psyclaw-theme.js +0 -89
- package/dist/src/psyclaw-theme.js.map +0 -1
- package/dist/src/research/brief.d.ts +0 -11
- package/dist/src/research/brief.js +0 -81
- package/dist/src/research/brief.js.map +0 -1
- package/dist/src/research/ledger.d.ts +0 -12
- package/dist/src/research/ledger.js +0 -43
- package/dist/src/research/ledger.js.map +0 -1
- package/dist/src/run.d.ts +0 -20
- package/dist/src/run.js +0 -34
- package/dist/src/run.js.map +0 -1
- package/dist/src/setup.d.ts +0 -54
- package/dist/src/setup.js +0 -221
- package/dist/src/setup.js.map +0 -1
- package/dist/src/skills/contracts.d.ts +0 -105
- package/dist/src/skills/contracts.js +0 -3
- package/dist/src/skills/contracts.js.map +0 -1
- package/dist/src/skills/preflight.d.ts +0 -10
- package/dist/src/skills/preflight.js +0 -39
- package/dist/src/skills/preflight.js.map +0 -1
- package/dist/src/skills/registry.d.ts +0 -28
- package/dist/src/skills/registry.js +0 -556
- package/dist/src/skills/registry.js.map +0 -1
- package/dist/src/style/artifact.d.ts +0 -6
- package/dist/src/style/artifact.js +0 -61
- package/dist/src/style/artifact.js.map +0 -1
- package/dist/src/style/cli-ui.d.ts +0 -41
- package/dist/src/style/cli-ui.js +0 -119
- package/dist/src/style/cli-ui.js.map +0 -1
- package/dist/src/style/contracts.d.ts +0 -34
- package/dist/src/style/contracts.js +0 -2
- package/dist/src/style/contracts.js.map +0 -1
- package/dist/src/style/registry.d.ts +0 -16
- package/dist/src/style/registry.js +0 -50
- package/dist/src/style/registry.js.map +0 -1
- package/dist/src/trust/manifest.d.ts +0 -42
- package/dist/src/trust/manifest.js +0 -53
- package/dist/src/trust/manifest.js.map +0 -1
- package/dist/src/trust/sbom.d.ts +0 -54
- package/dist/src/trust/sbom.js +0 -77
- package/dist/src/trust/sbom.js.map +0 -1
- package/dist/src/tui/app.d.ts +0 -3
- package/dist/src/tui/app.js +0 -103
- package/dist/src/tui/app.js.map +0 -1
- package/dist/src/tui/model.d.ts +0 -13
- package/dist/src/tui/model.js +0 -25
- package/dist/src/tui/model.js.map +0 -1
- package/dist/src/updates/check.d.ts +0 -43
- package/dist/src/updates/check.js +0 -68
- package/dist/src/updates/check.js.map +0 -1
- package/dist/src/updates/manifest.d.ts +0 -19
- package/dist/src/updates/manifest.js +0 -58
- package/dist/src/updates/manifest.js.map +0 -1
- package/dist/src/updates/registry.d.ts +0 -21
- package/dist/src/updates/registry.js +0 -82
- package/dist/src/updates/registry.js.map +0 -1
- package/dist/src/updates/skills.d.ts +0 -15
- package/dist/src/updates/skills.js +0 -39
- package/dist/src/updates/skills.js.map +0 -1
- package/dist/src/updates/status.d.ts +0 -7
- package/dist/src/updates/status.js +0 -37
- package/dist/src/updates/status.js.map +0 -1
- package/dist/src/updates/update.d.ts +0 -43
- package/dist/src/updates/update.js +0 -126
- package/dist/src/updates/update.js.map +0 -1
- package/dist/src/wizard.d.ts +0 -7
- package/dist/src/wizard.js +0 -90
- package/dist/src/wizard.js.map +0 -1
- package/dist/src/workflows/analysis-delegation.d.ts +0 -3
- package/dist/src/workflows/analysis-delegation.js +0 -96
- package/dist/src/workflows/analysis-delegation.js.map +0 -1
- package/dist/src/workflows/expert-review-runner.d.ts +0 -29
- package/dist/src/workflows/expert-review-runner.js +0 -35
- package/dist/src/workflows/expert-review-runner.js.map +0 -1
- package/dist/src/workflows/expert-review.d.ts +0 -140
- package/dist/src/workflows/expert-review.js +0 -111
- package/dist/src/workflows/expert-review.js.map +0 -1
- package/dist/src/workflows/institutional-fulltext.d.ts +0 -16
- package/dist/src/workflows/institutional-fulltext.js +0 -110
- package/dist/src/workflows/institutional-fulltext.js.map +0 -1
- package/dist/src/workflows/literature-review.d.ts +0 -3
- package/dist/src/workflows/literature-review.js +0 -76
- package/dist/src/workflows/literature-review.js.map +0 -1
- package/dist/src/workflows/meta-analysis.d.ts +0 -38
- package/dist/src/workflows/meta-analysis.js +0 -185
- package/dist/src/workflows/meta-analysis.js.map +0 -1
- package/dist/src/workflows/publish.d.ts +0 -51
- package/dist/src/workflows/publish.js +0 -153
- package/dist/src/workflows/publish.js.map +0 -1
- package/dist/src/workflows/spec.d.ts +0 -51
- package/dist/src/workflows/spec.js +0 -157
- package/dist/src/workflows/spec.js.map +0 -1
- package/dist/src/workflows/style-contract.d.ts +0 -24
- package/dist/src/workflows/style-contract.js +0 -23
- package/dist/src/workflows/style-contract.js.map +0 -1
- package/dist/src/workflows/writing-review.d.ts +0 -15
- package/dist/src/workflows/writing-review.js +0 -82
- package/dist/src/workflows/writing-review.js.map +0 -1
- 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
|