toolfunnel 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +235 -167
- package/SECURITY.md +84 -0
- package/bin/toolfunnel.js +451 -147
- package/docs/KNOWN_BUGS.md +80 -33
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +84 -0
- package/docs/packaging.md +115 -115
- package/hooks/hooks.manifest.json +27 -27
- package/hooks/scripts/example-audit.js +21 -21
- package/hooks/scripts/example-deny-dangerous.js +39 -31
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -0
- package/mcp/servers/mock-upstream/server.js +222 -12
- package/package.json +67 -65
- package/src/auth/config.js +174 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +353 -344
- package/src/core/child-reaper.js +61 -0
- package/src/core/config-home.js +134 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +329 -275
- package/src/core/hook-loader.js +824 -791
- package/src/core/hook-runner.js +566 -346
- package/src/core/logger.js +297 -171
- package/src/core/matcher.js +100 -51
- package/src/core/metrics.js +75 -75
- package/src/core/rename-migration.js +163 -0
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -77
- package/src/extend/add-hook.md +152 -142
- package/src/extend/add-mcp.md +119 -121
- package/src/extend/configure.md +88 -0
- package/src/extend/create-tool.md +168 -152
- package/src/extend/howto.js +102 -98
- package/src/extend/package.md +190 -187
- package/src/extend/wrap.md +99 -0
- package/src/mcp/aggregator.js +1674 -807
- package/src/mcp/expose-store.js +577 -430
- package/src/mcp/gated-run.js +210 -194
- package/src/mcp/http-transport.js +1563 -1012
- package/src/mcp/mcp-client.js +1070 -530
- package/src/mcp/modern.js +406 -0
- package/src/mcp/protocol.js +468 -466
- package/src/mcp/server.js +3112 -1405
- package/src/tools/registry.js +661 -513
- package/src/tools/tool-state.js +170 -104
- package/src/ui/public/app.js +1187 -1055
- package/src/ui/public/index.html +286 -249
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1616 -1190
- package/tools/scripts/base64.js +139 -139
- package/tools/scripts/danger.js +61 -61
- package/tools/scripts/echo.js +39 -39
- package/tools/scripts/hash.js +101 -101
- package/tools/scripts/json.js +109 -109
- package/tools/scripts/text-stats.js +131 -131
- package/tools/scripts/tf-env.js +43 -43
- package/tools/scripts/tf-hook-add.js +151 -151
- package/tools/scripts/tf-hook-set.js +120 -112
- package/tools/scripts/tf-list.js +192 -192
- package/tools/scripts/tf-log.js +86 -86
- package/tools/scripts/tf-mcp-add.js +117 -117
- package/tools/scripts/tf-mcp-set.js +121 -102
- package/tools/scripts/tf-pack.js +237 -237
- package/tools/scripts/tf-tool-add.js +134 -129
- package/tools/scripts/tf-tool-set.js +142 -142
- package/tools/scripts/tf-wrap.js +103 -0
- package/tools/scripts/uuid.js +102 -102
- package/tools/tools.register.json +14 -3
- package/src/core/tool-registry.js +0 -180
- package/src/packages/packager.js +0 -715
- package/src/tools/drift.js +0 -138
package/tools/scripts/uuid.js
CHANGED
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* uuid-tool.js
|
|
6
|
-
*
|
|
7
|
-
* Purpose
|
|
8
|
-
* -------
|
|
9
|
-
* Generate one or more RFC 4122 v4 UUIDs via the Node built-in
|
|
10
|
-
* `node:crypto.randomUUID()`. Useful from the Tool Manager whenever a fresh
|
|
11
|
-
* identifier is needed. No network, no filesystem, no new deps.
|
|
12
|
-
*
|
|
13
|
-
* Contract with the register (the register's `defaultRunScript`)
|
|
14
|
-
* ----------------------------------------------------------
|
|
15
|
-
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
16
|
-
* - Args: { count?: <integer> }
|
|
17
|
-
* count is clamped to the range [1, 100] so a runaway request cannot ask
|
|
18
|
-
* for an unbounded number.
|
|
19
|
-
* - Prints a SINGLE JSON object to stdout and exits 0
|
|
20
|
-
* on bad input (reported as { ok:false, error }), never a thrown exception.
|
|
21
|
-
*
|
|
22
|
-
* Output (stdout), exactly one JSON object:
|
|
23
|
-
* success: { ok:true, uuids: [ "...", ... ] }
|
|
24
|
-
* failure: { ok:false, error }
|
|
25
|
-
*
|
|
26
|
-
* Safety invariants (mirror the isolation rule):
|
|
27
|
-
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
28
|
-
* - count is hard-capped at 100; a non-integer / out-of-range count is an
|
|
29
|
-
* error rather than a silent surprise.
|
|
30
|
-
* - NEVER throws for ordinary bad input
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const crypto = require('node:crypto');
|
|
34
|
-
|
|
35
|
-
const MAX_COUNT = 100;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
39
|
-
* @returns {{ value: any } | { parseError: string }}
|
|
40
|
-
*/
|
|
41
|
-
function parseStructuredArgs() {
|
|
42
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
43
|
-
if (raw === undefined || raw === null || raw === '') {
|
|
44
|
-
return { value: null };
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
return { value: JSON.parse(raw) };
|
|
48
|
-
} catch (_err) {
|
|
49
|
-
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
55
|
-
*
|
|
56
|
-
* @param {any} args may be null (treated as { count: 1 })
|
|
57
|
-
* @returns {object} the JSON-serialisable result
|
|
58
|
-
*/
|
|
59
|
-
function run(args) {
|
|
60
|
-
// No args (or null) is valid
|
|
61
|
-
let count = 1;
|
|
62
|
-
if (args !== null && args !== undefined) {
|
|
63
|
-
if (typeof args !== 'object' || Array.isArray(args)) {
|
|
64
|
-
return { ok: false, error: 'args must be an object { count? }' };
|
|
65
|
-
}
|
|
66
|
-
if (args.count !== undefined && args.count !== null) {
|
|
67
|
-
if (typeof args.count !== 'number' || !Number.isInteger(args.count)) {
|
|
68
|
-
return { ok: false, error: 'count must be an integer' };
|
|
69
|
-
}
|
|
70
|
-
if (args.count < 1) {
|
|
71
|
-
return { ok: false, error: 'count must be >= 1' };
|
|
72
|
-
}
|
|
73
|
-
if (args.count > MAX_COUNT) {
|
|
74
|
-
return { ok: false, error: `count must be <= ${MAX_COUNT}` };
|
|
75
|
-
}
|
|
76
|
-
count = args.count;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
const uuids = [];
|
|
80
|
-
for (let i = 0; i < count; i += 1) {
|
|
81
|
-
uuids.push(crypto.randomUUID());
|
|
82
|
-
}
|
|
83
|
-
return { ok: true, uuids };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function main() {
|
|
87
|
-
const parsed = parseStructuredArgs();
|
|
88
|
-
let payload;
|
|
89
|
-
if (parsed.parseError) {
|
|
90
|
-
payload = { ok: false, error: parsed.parseError };
|
|
91
|
-
} else {
|
|
92
|
-
try {
|
|
93
|
-
payload = run(parsed.value);
|
|
94
|
-
} catch (err) {
|
|
95
|
-
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
99
|
-
process.exit(0);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* uuid-tool.js - a safe, dependency-free local UUID generator.
|
|
6
|
+
*
|
|
7
|
+
* Purpose
|
|
8
|
+
* -------
|
|
9
|
+
* Generate one or more RFC 4122 v4 UUIDs via the Node built-in
|
|
10
|
+
* `node:crypto.randomUUID()`. Useful from the Tool Manager whenever a fresh
|
|
11
|
+
* identifier is needed. No network, no filesystem, no new deps.
|
|
12
|
+
*
|
|
13
|
+
* Contract with the register (the register's `defaultRunScript`)
|
|
14
|
+
* ----------------------------------------------------------
|
|
15
|
+
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
16
|
+
* - Args: { count?: <integer> } - how many UUIDs to mint. Defaults to 1.
|
|
17
|
+
* count is clamped to the range [1, 100] so a runaway request cannot ask
|
|
18
|
+
* for an unbounded number.
|
|
19
|
+
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
|
|
20
|
+
* on bad input (reported as { ok:false, error }), never a thrown exception.
|
|
21
|
+
*
|
|
22
|
+
* Output (stdout), exactly one JSON object:
|
|
23
|
+
* success: { ok:true, uuids: [ "...", ... ] }
|
|
24
|
+
* failure: { ok:false, error }
|
|
25
|
+
*
|
|
26
|
+
* Safety invariants (mirror the isolation rule):
|
|
27
|
+
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
28
|
+
* - count is hard-capped at 100; a non-integer / out-of-range count is an
|
|
29
|
+
* error rather than a silent surprise.
|
|
30
|
+
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const crypto = require('node:crypto');
|
|
34
|
+
|
|
35
|
+
const MAX_COUNT = 100;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
39
|
+
* @returns {{ value: any } | { parseError: string }}
|
|
40
|
+
*/
|
|
41
|
+
function parseStructuredArgs() {
|
|
42
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
43
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
44
|
+
return { value: null };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return { value: JSON.parse(raw) };
|
|
48
|
+
} catch (_err) {
|
|
49
|
+
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
55
|
+
*
|
|
56
|
+
* @param {any} args may be null (treated as { count: 1 })
|
|
57
|
+
* @returns {object} the JSON-serialisable result
|
|
58
|
+
*/
|
|
59
|
+
function run(args) {
|
|
60
|
+
// No args (or null) is valid - default to a single UUID.
|
|
61
|
+
let count = 1;
|
|
62
|
+
if (args !== null && args !== undefined) {
|
|
63
|
+
if (typeof args !== 'object' || Array.isArray(args)) {
|
|
64
|
+
return { ok: false, error: 'args must be an object { count? }' };
|
|
65
|
+
}
|
|
66
|
+
if (args.count !== undefined && args.count !== null) {
|
|
67
|
+
if (typeof args.count !== 'number' || !Number.isInteger(args.count)) {
|
|
68
|
+
return { ok: false, error: 'count must be an integer' };
|
|
69
|
+
}
|
|
70
|
+
if (args.count < 1) {
|
|
71
|
+
return { ok: false, error: 'count must be >= 1' };
|
|
72
|
+
}
|
|
73
|
+
if (args.count > MAX_COUNT) {
|
|
74
|
+
return { ok: false, error: `count must be <= ${MAX_COUNT}` };
|
|
75
|
+
}
|
|
76
|
+
count = args.count;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const uuids = [];
|
|
80
|
+
for (let i = 0; i < count; i += 1) {
|
|
81
|
+
uuids.push(crypto.randomUUID());
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, uuids };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function main() {
|
|
87
|
+
const parsed = parseStructuredArgs();
|
|
88
|
+
let payload;
|
|
89
|
+
if (parsed.parseError) {
|
|
90
|
+
payload = { ok: false, error: parsed.parseError };
|
|
91
|
+
} else {
|
|
92
|
+
try {
|
|
93
|
+
payload = run(parsed.value);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"description": "Tool register for the gateway
|
|
3
|
+
"description": "Tool register for the gateway - built-in demo scripts.",
|
|
4
4
|
"tools": [
|
|
5
5
|
{
|
|
6
6
|
"id": "echo",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"name": "tf_tool_set",
|
|
96
96
|
"summary": "Activate, deactivate, promote (hot), hide, or remove a register tool.",
|
|
97
97
|
"category": "management",
|
|
98
|
-
"instructions": "Args: { id, action:'enable'|'disable'|'hot'|'unhot'|'hide'|'unhide'|'remove' }. enable/disable flips the active overlay (default ON)
|
|
98
|
+
"instructions": "Args: { id, action:'enable'|'disable'|'hot'|'unhot'|'hide'|'unhide'|'remove' }. enable/disable flips the active overlay (default ON) - disabled tools drop from toolfunnel_list_tools. hot/unhot promote/demote on the TOP-LEVEL every-turn surface (tools/list), making it directly callable (default OFF). hide/unhide declutter the MANAGER views only (tf_list + UI) and do NOT change what the AI sees. The id may be a local tool id, an upstream surfaced name, or a meta-tool name. remove deletes the register entry and purges its state key. All LIVE (no restart).",
|
|
99
99
|
"invoke": {
|
|
100
100
|
"type": "script",
|
|
101
101
|
"path": "scripts/tf-tool-set.js"
|
|
@@ -123,6 +123,17 @@
|
|
|
123
123
|
"path": "scripts/tf-mcp-set.js"
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
|
+
{
|
|
127
|
+
"id": "tf_wrap",
|
|
128
|
+
"name": "tf_wrap",
|
|
129
|
+
"summary": "Wrap ONE attached upstream MCP: its tools become the entire surface (transparent passthrough).",
|
|
130
|
+
"category": "management",
|
|
131
|
+
"instructions": "Args: {} reports status ({ wrapping, upstreams }). { off:true } clears the wrap. { upstream:'<id>', confirm:true } sets it: that upstream's tools become the ENTIRE advertised surface under their ORIGINAL names, everything else (meta-tools INCLUDED) is hidden and uncallable, and every call still fires the PreToolUse gate. Because the gateway answers both MCP eras, this keeps a legacy-only MCP working with 2026-07-28-era clients. WARNING (why confirm is required): the wrap hides tf_wrap itself, so YOU cannot undo it in-band afterwards - recovery is the config web UI (--ui) or `toolfunnel wrap --off` on a terminal. A running host applies the change live.",
|
|
132
|
+
"invoke": {
|
|
133
|
+
"type": "script",
|
|
134
|
+
"path": "scripts/tf-wrap.js"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
126
137
|
{
|
|
127
138
|
"id": "tf_hook_add",
|
|
128
139
|
"name": "tf_hook_add",
|
|
@@ -150,7 +161,7 @@
|
|
|
150
161
|
"name": "tf_list",
|
|
151
162
|
"summary": "List register tools, upstream MCPs, or hooks with their active state.",
|
|
152
163
|
"category": "management",
|
|
153
|
-
"instructions": "Args: { kind:'tools'|'mcps'|'hooks' }. tools
|
|
164
|
+
"instructions": "Args: { kind:'tools'|'mcps'|'hooks' }. tools -> register briefs + enabled/hidden overlay; mcps -> upstreams + expose selections; hooks -> configured + auto-detected inventory with live enabled state. Read-only.",
|
|
154
165
|
"invoke": {
|
|
155
166
|
"type": "script",
|
|
156
167
|
"path": "scripts/tf-list.js"
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* tool-registry.js — backend-agnostic tool catalogue for the host loop.
|
|
5
|
-
*
|
|
6
|
-
* The host asks the registry for tool definitions to hand to a backend
|
|
7
|
-
* (see ARCHITECTURE.md: `backend.send({ system, messages, tools })`). When the
|
|
8
|
-
* model requests a tool by name, the host executes it here. The registry holds
|
|
9
|
-
* NO host imports so it runs headless under `node --test`.
|
|
10
|
-
*
|
|
11
|
-
* A tool is registered as: register(name, { description, inputSchema, run })
|
|
12
|
-
* - description: human/model-facing string
|
|
13
|
-
* - inputSchema: a JSON-schema-ish object describing the input (handed to the backend)
|
|
14
|
-
* - run(input): the implementation; may be sync or async; its return value is the result
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const fs = require('node:fs');
|
|
18
|
-
const path = require('node:path');
|
|
19
|
-
|
|
20
|
-
class ToolRegistry {
|
|
21
|
-
constructor() {
|
|
22
|
-
// name -> { description, inputSchema, run }
|
|
23
|
-
this._tools = new Map();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Register a tool under a unique name.
|
|
28
|
-
* Throws on a bad shape so misconfiguration is caught at wiring time, not at
|
|
29
|
-
* model-call time (the loop must never receive a half-formed tool).
|
|
30
|
-
*
|
|
31
|
-
* @param {string} name
|
|
32
|
-
* @param {{ description?: string, inputSchema?: object, run: (input:any)=>any }} def
|
|
33
|
-
* @returns {ToolRegistry} this (for chaining)
|
|
34
|
-
*/
|
|
35
|
-
register(name, def) {
|
|
36
|
-
if (typeof name !== 'string' || name.length === 0) {
|
|
37
|
-
throw new Error('ToolRegistry.register: name must be a non-empty string');
|
|
38
|
-
}
|
|
39
|
-
if (!def || typeof def !== 'object') {
|
|
40
|
-
throw new Error(`ToolRegistry.register("${name}"): definition object is required`);
|
|
41
|
-
}
|
|
42
|
-
if (typeof def.run !== 'function') {
|
|
43
|
-
throw new Error(`ToolRegistry.register("${name}"): "run" must be a function`);
|
|
44
|
-
}
|
|
45
|
-
if (this._tools.has(name)) {
|
|
46
|
-
throw new Error(`ToolRegistry.register("${name}"): tool already registered`);
|
|
47
|
-
}
|
|
48
|
-
this._tools.set(name, {
|
|
49
|
-
description: typeof def.description === 'string' ? def.description : '',
|
|
50
|
-
// Default to an open object schema so a tool without a declared schema
|
|
51
|
-
// still produces a valid definition for the backend.
|
|
52
|
-
inputSchema:
|
|
53
|
-
def.inputSchema && typeof def.inputSchema === 'object'
|
|
54
|
-
? def.inputSchema
|
|
55
|
-
: { type: 'object', properties: {} },
|
|
56
|
-
run: def.run,
|
|
57
|
-
});
|
|
58
|
-
return this;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** @param {string} name @returns {boolean} */
|
|
62
|
-
has(name) {
|
|
63
|
-
return this._tools.has(name);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Execute a registered tool by name. Always returns a Promise so the host
|
|
68
|
-
* loop can `await` uniformly regardless of whether `run` was sync or async.
|
|
69
|
-
* Throws a clear error if the tool is unknown (the host turns this into a
|
|
70
|
-
* tool result / PostToolUse feedback rather than crashing the loop).
|
|
71
|
-
*
|
|
72
|
-
* @param {string} name
|
|
73
|
-
* @param {any} input
|
|
74
|
-
* @returns {Promise<any>} the tool's result
|
|
75
|
-
*/
|
|
76
|
-
async execute(name, input) {
|
|
77
|
-
const tool = this._tools.get(name);
|
|
78
|
-
if (!tool) {
|
|
79
|
-
const known = this.list().map((t) => t.name);
|
|
80
|
-
throw new Error(
|
|
81
|
-
`ToolRegistry.execute: unknown tool "${name}". ` +
|
|
82
|
-
`Registered tools: ${known.length ? known.join(', ') : '(none)'}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
// Await covers both sync return values and returned Promises.
|
|
86
|
-
return await tool.run(input);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Tool definitions array, shaped for handing to a backend:
|
|
91
|
-
* `[{ name, description, inputSchema }]`. The `run` function is deliberately
|
|
92
|
-
* omitted — the backend never needs (or should see) the implementation.
|
|
93
|
-
*
|
|
94
|
-
* @returns {Array<{ name: string, description: string, inputSchema: object }>}
|
|
95
|
-
*/
|
|
96
|
-
list() {
|
|
97
|
-
const out = [];
|
|
98
|
-
for (const [name, tool] of this._tools) {
|
|
99
|
-
out.push({
|
|
100
|
-
name,
|
|
101
|
-
description: tool.description,
|
|
102
|
-
inputSchema: tool.inputSchema,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* makeDemoRegistry — a ToolRegistry preloaded with two demo tools used by the
|
|
111
|
-
* host tests (host.test.js drives the lifecycle through these).
|
|
112
|
-
*
|
|
113
|
-
* echo — returns its input unchanged (trivial, deterministic).
|
|
114
|
-
* readFileSandboxed — reads a UTF-8 file, but ONLY within `sandboxRoot`,
|
|
115
|
-
* rejecting any path that escapes the sandbox. This is
|
|
116
|
-
* the same defense-in-depth posture the hook loader's
|
|
117
|
-
* writeScript guard uses (HOOK_ENGINE.md §8) — a tool the
|
|
118
|
-
* model can call must not become a filesystem escape.
|
|
119
|
-
*
|
|
120
|
-
* @param {object} [opts]
|
|
121
|
-
* @param {string} [opts.sandboxRoot=process.cwd()] absolute root for readFileSandboxed
|
|
122
|
-
* @returns {ToolRegistry}
|
|
123
|
-
*/
|
|
124
|
-
function makeDemoRegistry(opts) {
|
|
125
|
-
const sandboxRoot = path.resolve((opts && opts.sandboxRoot) || process.cwd());
|
|
126
|
-
const registry = new ToolRegistry();
|
|
127
|
-
|
|
128
|
-
registry.register('echo', {
|
|
129
|
-
description: 'Returns its input unchanged. Useful for testing the tool loop.',
|
|
130
|
-
inputSchema: {
|
|
131
|
-
type: 'object',
|
|
132
|
-
description: 'Any JSON value passed through as-is.',
|
|
133
|
-
properties: {},
|
|
134
|
-
additionalProperties: true,
|
|
135
|
-
},
|
|
136
|
-
run: (input) => input,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
registry.register('readFileSandboxed', {
|
|
140
|
-
description:
|
|
141
|
-
'Reads a UTF-8 text file located within the sandbox root. ' +
|
|
142
|
-
'Rejects absolute paths and any relative path that escapes the sandbox.',
|
|
143
|
-
inputSchema: {
|
|
144
|
-
type: 'object',
|
|
145
|
-
properties: {
|
|
146
|
-
path: {
|
|
147
|
-
type: 'string',
|
|
148
|
-
description: 'Path to read, relative to the sandbox root.',
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
required: ['path'],
|
|
152
|
-
},
|
|
153
|
-
run: (input) => {
|
|
154
|
-
const requested = input && input.path;
|
|
155
|
-
if (typeof requested !== 'string' || requested.length === 0) {
|
|
156
|
-
throw new Error('readFileSandboxed: "path" (non-empty string) is required');
|
|
157
|
-
}
|
|
158
|
-
// Reject absolute paths outright; everything must be relative to the sandbox.
|
|
159
|
-
if (path.isAbsolute(requested)) {
|
|
160
|
-
throw new Error(
|
|
161
|
-
`readFileSandboxed: absolute paths are not allowed ("${requested}")`
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
// Resolve against the sandbox, then confirm the result is still inside it.
|
|
165
|
-
const resolved = path.resolve(sandboxRoot, requested);
|
|
166
|
-
const rel = path.relative(sandboxRoot, resolved);
|
|
167
|
-
// rel starting with ".." (or an absolute path on a different drive) means escape.
|
|
168
|
-
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`readFileSandboxed: path escapes sandbox root ("${requested}")`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
return fs.readFileSync(resolved, 'utf8');
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
return registry;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
module.exports = { ToolRegistry, makeDemoRegistry };
|