toolfunnel 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +228 -231
  2. package/SECURITY.md +84 -0
  3. package/bin/toolfunnel.js +451 -372
  4. package/docs/KNOWN_BUGS.md +80 -99
  5. package/docs/MANUAL.pdf +0 -0
  6. package/docs/design.md +84 -70
  7. package/docs/packaging.md +115 -115
  8. package/hooks/hooks.manifest.json +27 -27
  9. package/hooks/scripts/example-audit.js +21 -21
  10. package/hooks/scripts/example-deny-dangerous.js +39 -31
  11. package/mcp/expose.example.json +30 -30
  12. package/mcp/servers/mock-modern/server.js +147 -147
  13. package/mcp/servers/mock-upstream/server.js +380 -350
  14. package/package.json +3 -1
  15. package/src/auth/config.js +174 -174
  16. package/src/auth/install.js +181 -181
  17. package/src/auth/resource-server.js +353 -344
  18. package/src/core/child-reaper.js +61 -0
  19. package/src/core/config-home.js +134 -134
  20. package/src/core/events.js +105 -105
  21. package/src/core/hook-engine.js +329 -275
  22. package/src/core/hook-loader.js +824 -791
  23. package/src/core/hook-runner.js +566 -346
  24. package/src/core/logger.js +297 -171
  25. package/src/core/matcher.js +100 -51
  26. package/src/core/metrics.js +75 -75
  27. package/src/core/rename-migration.js +163 -0
  28. package/src/core/requires.js +138 -138
  29. package/src/core/server-config.js +98 -98
  30. package/src/extend/add-hook.md +152 -152
  31. package/src/extend/add-mcp.md +119 -122
  32. package/src/extend/configure.md +88 -87
  33. package/src/extend/create-tool.md +168 -168
  34. package/src/extend/howto.js +102 -102
  35. package/src/extend/package.md +190 -187
  36. package/src/extend/wrap.md +99 -99
  37. package/src/mcp/aggregator.js +1719 -1246
  38. package/src/mcp/expose-store.js +577 -460
  39. package/src/mcp/gated-run.js +210 -201
  40. package/src/mcp/http-transport.js +1563 -1311
  41. package/src/mcp/mcp-client.js +1070 -907
  42. package/src/mcp/modern.js +406 -372
  43. package/src/mcp/protocol.js +468 -468
  44. package/src/mcp/server.js +3112 -2688
  45. package/src/tools/registry.js +661 -513
  46. package/src/tools/tool-state.js +170 -170
  47. package/src/ui/public/app.js +1187 -1174
  48. package/src/ui/public/index.html +286 -286
  49. package/src/ui/public/styles.css +535 -535
  50. package/src/ui/server.js +1616 -1348
  51. package/tools/scripts/base64.js +139 -139
  52. package/tools/scripts/danger.js +61 -61
  53. package/tools/scripts/echo.js +39 -39
  54. package/tools/scripts/hash.js +101 -101
  55. package/tools/scripts/json.js +109 -109
  56. package/tools/scripts/text-stats.js +131 -131
  57. package/tools/scripts/tf-env.js +43 -43
  58. package/tools/scripts/tf-hook-add.js +151 -151
  59. package/tools/scripts/tf-hook-set.js +120 -112
  60. package/tools/scripts/tf-list.js +192 -192
  61. package/tools/scripts/tf-log.js +86 -86
  62. package/tools/scripts/tf-mcp-add.js +117 -117
  63. package/tools/scripts/tf-mcp-set.js +121 -121
  64. package/tools/scripts/tf-pack.js +237 -237
  65. package/tools/scripts/tf-tool-add.js +134 -134
  66. package/tools/scripts/tf-tool-set.js +142 -142
  67. package/tools/scripts/tf-wrap.js +103 -103
  68. package/tools/scripts/uuid.js +102 -102
  69. package/tools/tools.register.json +227 -227
  70. package/src/core/tool-registry.js +0 -180
  71. package/src/packages/packager.js +0 -715
  72. package/src/tools/drift.js +0 -138
@@ -1,227 +1,227 @@
1
- {
2
- "version": 1,
3
- "description": "Tool register for the gateway - built-in demo scripts.",
4
- "tools": [
5
- {
6
- "id": "echo",
7
- "name": "Echo",
8
- "summary": "Return the provided arguments back unchanged.",
9
- "category": "demo",
10
- "instructions": "Echoes its structured args back in the result. Pass any JSON value as args and the same value is returned. Useful as a connectivity / round-trip smoke test.",
11
- "invoke": {
12
- "type": "script",
13
- "path": "scripts/echo.js"
14
- }
15
- },
16
- {
17
- "id": "base64",
18
- "name": "Base64 Encode/Decode",
19
- "summary": "Encode text to Base64 or decode Base64 back to text.",
20
- "category": "encoding",
21
- "instructions": "Args: { op: \"encode\"|\"decode\", text: string }. Both args required. encode -> Base64 of the UTF-8 text; decode -> UTF-8 from valid Base64 (malformed input is reported, not mangled).",
22
- "invoke": {
23
- "type": "script",
24
- "path": "scripts/base64.js"
25
- }
26
- },
27
- {
28
- "id": "hash",
29
- "name": "Hash",
30
- "summary": "Compute a cryptographic digest (e.g. sha256) of input text.",
31
- "category": "crypto",
32
- "instructions": "Args: { algo?: \"sha256\"|\"sha1\"|\"md5\" (default \"sha256\"), text: string }. Returns the lowercase hex digest of the UTF-8 text.",
33
- "invoke": {
34
- "type": "script",
35
- "path": "scripts/hash.js"
36
- }
37
- },
38
- {
39
- "id": "uuid",
40
- "name": "UUID Generator",
41
- "summary": "Generate a random RFC 4122 v4 UUID.",
42
- "category": "generators",
43
- "instructions": "Args: { count?: number (default 1) }. Returns one or more freshly generated v4 UUID strings.",
44
- "invoke": {
45
- "type": "script",
46
- "path": "scripts/uuid.js"
47
- }
48
- },
49
- {
50
- "id": "json",
51
- "name": "JSON Format",
52
- "summary": "Validate and pretty-print a JSON string.",
53
- "category": "data",
54
- "instructions": "Args: { text: string, indent?: number (default 2) }. Parses the input; returns the re-serialised, indented JSON, or a clear parse error.",
55
- "invoke": {
56
- "type": "script",
57
- "path": "scripts/json.js"
58
- }
59
- },
60
- {
61
- "id": "text-stats",
62
- "name": "Text Stats",
63
- "summary": "Count characters, words, and lines in a block of text.",
64
- "category": "text",
65
- "instructions": "Args: { text: string }. Returns { chars, words, lines } computed from the input text.",
66
- "invoke": {
67
- "type": "script",
68
- "path": "scripts/text-stats.js"
69
- }
70
- },
71
- {
72
- "id": "danger",
73
- "name": "Danger Demo",
74
- "summary": "Demonstrate a gated, potentially destructive operation.",
75
- "category": "demo",
76
- "instructions": "A sample tool that represents a destructive action. It is intended to be exercised behind the gate so the gating path can be tested end to end. Pass { confirm: true } to proceed.",
77
- "invoke": {
78
- "type": "script",
79
- "path": "scripts/danger.js"
80
- }
81
- },
82
- {
83
- "id": "tf_tool_add",
84
- "name": "tf_tool_add",
85
- "summary": "Register a new tool entry in the gateway tool register.",
86
- "category": "management",
87
- "instructions": "Add a register entry. Args: { id, name, summary?, category?, instructions?, inputSchema?, invoke:{type:'script',path:'scripts/<file>'}|{type:'shell',command}, scriptText? }. inputSchema is a JSON Schema object for the tool's args - a HOT-promoted tool advertises it verbatim in the top-level tools/list, so author it if the tool may be promoted. If scriptText is given for a script invoke, the body is written to tools/scripts/<basename> first. Change is LIVE (no reconnect). Fails if id already exists.",
88
- "invoke": {
89
- "type": "script",
90
- "path": "scripts/tf-tool-add.js"
91
- }
92
- },
93
- {
94
- "id": "tf_tool_set",
95
- "name": "tf_tool_set",
96
- "summary": "Activate, deactivate, promote (hot), hide, or remove a register tool.",
97
- "category": "management",
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
- "invoke": {
100
- "type": "script",
101
- "path": "scripts/tf-tool-set.js"
102
- }
103
- },
104
- {
105
- "id": "tf_mcp_add",
106
- "name": "tf_mcp_add",
107
- "summary": "Register an upstream MCP (and optionally expose some of its tools).",
108
- "category": "management",
109
- "instructions": "Args: { id, command, args?, env?, transport?='stdio', enabled?=true, description?, expose?:[{tool, as?, category?, enabled?}] }. Adds an upstreams[] entry; each expose[] item adds a curated-direct selection. Curated-direct changes need an aggregator reconnect to surface.",
110
- "invoke": {
111
- "type": "script",
112
- "path": "scripts/tf-mcp-add.js"
113
- }
114
- },
115
- {
116
- "id": "tf_mcp_set",
117
- "name": "tf_mcp_set",
118
- "summary": "Enable, disable, or remove an upstream MCP.",
119
- "category": "management",
120
- "instructions": "Args: { id, action:'enable'|'disable'|'remove' }. enable/disable toggles the upstream; remove deletes it and cascades its expose entries. Reconnect needed for the curated-direct surface to change.",
121
- "invoke": {
122
- "type": "script",
123
- "path": "scripts/tf-mcp-set.js"
124
- }
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
- },
137
- {
138
- "id": "tf_hook_add",
139
- "name": "tf_hook_add",
140
- "summary": "Add a hook manifest entry (e.g. a PreToolUse gate).",
141
- "category": "management",
142
- "instructions": "Args: { id, event, matcher?, command, script?, timeout?, enabled?=true, description?, scriptText? }. id format '<kebab-event>/<name>'. command should use the portable ${HOOKS_DIR} token. If scriptText is given, the script is authored under hooks/scripts/ (path-guarded). enabled is the manifest seed; live toggles live in hooks.state.json.",
143
- "invoke": {
144
- "type": "script",
145
- "path": "scripts/tf-hook-add.js"
146
- }
147
- },
148
- {
149
- "id": "tf_hook_set",
150
- "name": "tf_hook_set",
151
- "summary": "Enable, disable, or remove a hook.",
152
- "category": "management",
153
- "instructions": "Args: { id, action:'enable'|'disable'|'remove' }. enable/disable writes the hooks.state.json overlay (wins over the manifest seed) and mirrors the manifest; remove deletes the manifest entry and purges the overlay key (the script file is left in place).",
154
- "invoke": {
155
- "type": "script",
156
- "path": "scripts/tf-hook-set.js"
157
- }
158
- },
159
- {
160
- "id": "tf_list",
161
- "name": "tf_list",
162
- "summary": "List register tools, upstream MCPs, or hooks with their active state.",
163
- "category": "management",
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.",
165
- "invoke": {
166
- "type": "script",
167
- "path": "scripts/tf-list.js"
168
- }
169
- },
170
- {
171
- "id": "tf_log",
172
- "name": "tf_log",
173
- "summary": "Enable, disable, or check the gateway activity/audit log.",
174
- "category": "management",
175
- "instructions": "Args: { action:'enable'|'disable'|'status', path? }. Toggles ToolFunnel's JSONL audit log (tool runs + gate allow/deny decisions). Default OFF. 'status' returns {enabled,path} + recent count. Like any tool, tf_log can itself be disabled to revoke logging control.",
176
- "invoke": {
177
- "type": "script",
178
- "path": "scripts/tf-log.js"
179
- }
180
- },
181
- {
182
- "id": "tf_pack",
183
- "name": "tf_pack",
184
- "summary": "Package the LIVE setup into a deployment-ready artifact under <home>/dist/ (portable config home, or a publishable npm package wrapping toolfunnel).",
185
- "category": "management",
186
- "instructions": "Args: { format?:'home'|'npm' (default 'home'), name?, version?, description?, out?, force? }. Snapshots the current setup - tools + scripts, hot/hidden curation (tools.state.json), upstream references (mcp/expose.json), policy hooks (THE GATE TRAVELS), and identity (toolfunnel.json, rewritten with the pack's name/version; a declared `requires` travels too) - into <home>/dist/<out||name>/, a SEPARATE location from the live config. format 'home' = a portable config home (zip / git init / run via --config-dir). format 'npm' = a publishable package: generated package.json with toolfunnel as a caret DEPENDENCY (depend, never copy), a 2-line bin launcher pointing --config-dir at the bundled home/, and a README stub - then `cd dist/<name> && npm publish`. NEVER ships auth/ or logs/. Refuses a non-empty destination without force:true. out is a folder NAME under dist/, never a path.",
187
- "inputSchema": {
188
- "type": "object",
189
- "properties": {
190
- "format": {
191
- "type": "string",
192
- "enum": [
193
- "home",
194
- "npm"
195
- ],
196
- "description": "Artifact type: portable config home (default) or publishable npm package."
197
- },
198
- "name": {
199
- "type": "string",
200
- "description": "Pack/package name (default: the home's serverName)."
201
- },
202
- "version": {
203
- "type": "string",
204
- "description": "Pack version (default: the home's serverVersion, else 0.1.0)."
205
- },
206
- "description": {
207
- "type": "string",
208
- "description": "npm format: the package.json description."
209
- },
210
- "out": {
211
- "type": "string",
212
- "description": "Folder NAME under <home>/dist/ (default: name)."
213
- },
214
- "force": {
215
- "type": "boolean",
216
- "description": "Overwrite files in an existing non-empty destination."
217
- }
218
- },
219
- "additionalProperties": false
220
- },
221
- "invoke": {
222
- "type": "script",
223
- "path": "scripts/tf-pack.js"
224
- }
225
- }
226
- ]
227
- }
1
+ {
2
+ "version": 1,
3
+ "description": "Tool register for the gateway - built-in demo scripts.",
4
+ "tools": [
5
+ {
6
+ "id": "echo",
7
+ "name": "Echo",
8
+ "summary": "Return the provided arguments back unchanged.",
9
+ "category": "demo",
10
+ "instructions": "Echoes its structured args back in the result. Pass any JSON value as args and the same value is returned. Useful as a connectivity / round-trip smoke test.",
11
+ "invoke": {
12
+ "type": "script",
13
+ "path": "scripts/echo.js"
14
+ }
15
+ },
16
+ {
17
+ "id": "base64",
18
+ "name": "Base64 Encode/Decode",
19
+ "summary": "Encode text to Base64 or decode Base64 back to text.",
20
+ "category": "encoding",
21
+ "instructions": "Args: { op: \"encode\"|\"decode\", text: string }. Both args required. encode -> Base64 of the UTF-8 text; decode -> UTF-8 from valid Base64 (malformed input is reported, not mangled).",
22
+ "invoke": {
23
+ "type": "script",
24
+ "path": "scripts/base64.js"
25
+ }
26
+ },
27
+ {
28
+ "id": "hash",
29
+ "name": "Hash",
30
+ "summary": "Compute a cryptographic digest (e.g. sha256) of input text.",
31
+ "category": "crypto",
32
+ "instructions": "Args: { algo?: \"sha256\"|\"sha1\"|\"md5\" (default \"sha256\"), text: string }. Returns the lowercase hex digest of the UTF-8 text.",
33
+ "invoke": {
34
+ "type": "script",
35
+ "path": "scripts/hash.js"
36
+ }
37
+ },
38
+ {
39
+ "id": "uuid",
40
+ "name": "UUID Generator",
41
+ "summary": "Generate a random RFC 4122 v4 UUID.",
42
+ "category": "generators",
43
+ "instructions": "Args: { count?: number (default 1) }. Returns one or more freshly generated v4 UUID strings.",
44
+ "invoke": {
45
+ "type": "script",
46
+ "path": "scripts/uuid.js"
47
+ }
48
+ },
49
+ {
50
+ "id": "json",
51
+ "name": "JSON Format",
52
+ "summary": "Validate and pretty-print a JSON string.",
53
+ "category": "data",
54
+ "instructions": "Args: { text: string, indent?: number (default 2) }. Parses the input; returns the re-serialised, indented JSON, or a clear parse error.",
55
+ "invoke": {
56
+ "type": "script",
57
+ "path": "scripts/json.js"
58
+ }
59
+ },
60
+ {
61
+ "id": "text-stats",
62
+ "name": "Text Stats",
63
+ "summary": "Count characters, words, and lines in a block of text.",
64
+ "category": "text",
65
+ "instructions": "Args: { text: string }. Returns { chars, words, lines } computed from the input text.",
66
+ "invoke": {
67
+ "type": "script",
68
+ "path": "scripts/text-stats.js"
69
+ }
70
+ },
71
+ {
72
+ "id": "danger",
73
+ "name": "Danger Demo",
74
+ "summary": "Demonstrate a gated, potentially destructive operation.",
75
+ "category": "demo",
76
+ "instructions": "A sample tool that represents a destructive action. It is intended to be exercised behind the gate so the gating path can be tested end to end. Pass { confirm: true } to proceed.",
77
+ "invoke": {
78
+ "type": "script",
79
+ "path": "scripts/danger.js"
80
+ }
81
+ },
82
+ {
83
+ "id": "tf_tool_add",
84
+ "name": "tf_tool_add",
85
+ "summary": "Register a new tool entry in the gateway tool register.",
86
+ "category": "management",
87
+ "instructions": "Add a register entry. Args: { id, name, summary?, category?, instructions?, inputSchema?, invoke:{type:'script',path:'scripts/<file>'}|{type:'shell',command}, scriptText? }. inputSchema is a JSON Schema object for the tool's args - a HOT-promoted tool advertises it verbatim in the top-level tools/list, so author it if the tool may be promoted. If scriptText is given for a script invoke, the body is written to tools/scripts/<basename> first. Change is LIVE (no reconnect). Fails if id already exists.",
88
+ "invoke": {
89
+ "type": "script",
90
+ "path": "scripts/tf-tool-add.js"
91
+ }
92
+ },
93
+ {
94
+ "id": "tf_tool_set",
95
+ "name": "tf_tool_set",
96
+ "summary": "Activate, deactivate, promote (hot), hide, or remove a register tool.",
97
+ "category": "management",
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
+ "invoke": {
100
+ "type": "script",
101
+ "path": "scripts/tf-tool-set.js"
102
+ }
103
+ },
104
+ {
105
+ "id": "tf_mcp_add",
106
+ "name": "tf_mcp_add",
107
+ "summary": "Register an upstream MCP (and optionally expose some of its tools).",
108
+ "category": "management",
109
+ "instructions": "Args: { id, command, args?, env?, transport?='stdio', enabled?=true, description?, expose?:[{tool, as?, category?, enabled?}] }. Adds an upstreams[] entry; each expose[] item adds a curated-direct selection. Curated-direct changes need an aggregator reconnect to surface.",
110
+ "invoke": {
111
+ "type": "script",
112
+ "path": "scripts/tf-mcp-add.js"
113
+ }
114
+ },
115
+ {
116
+ "id": "tf_mcp_set",
117
+ "name": "tf_mcp_set",
118
+ "summary": "Enable, disable, or remove an upstream MCP.",
119
+ "category": "management",
120
+ "instructions": "Args: { id, action:'enable'|'disable'|'remove' }. enable/disable toggles the upstream; remove deletes it and cascades its expose entries. Reconnect needed for the curated-direct surface to change.",
121
+ "invoke": {
122
+ "type": "script",
123
+ "path": "scripts/tf-mcp-set.js"
124
+ }
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
+ },
137
+ {
138
+ "id": "tf_hook_add",
139
+ "name": "tf_hook_add",
140
+ "summary": "Add a hook manifest entry (e.g. a PreToolUse gate).",
141
+ "category": "management",
142
+ "instructions": "Args: { id, event, matcher?, command, script?, timeout?, enabled?=true, description?, scriptText? }. id format '<kebab-event>/<name>'. command should use the portable ${HOOKS_DIR} token. If scriptText is given, the script is authored under hooks/scripts/ (path-guarded). enabled is the manifest seed; live toggles live in hooks.state.json.",
143
+ "invoke": {
144
+ "type": "script",
145
+ "path": "scripts/tf-hook-add.js"
146
+ }
147
+ },
148
+ {
149
+ "id": "tf_hook_set",
150
+ "name": "tf_hook_set",
151
+ "summary": "Enable, disable, or remove a hook.",
152
+ "category": "management",
153
+ "instructions": "Args: { id, action:'enable'|'disable'|'remove' }. enable/disable writes the hooks.state.json overlay (wins over the manifest seed) and mirrors the manifest; remove deletes the manifest entry and purges the overlay key (the script file is left in place).",
154
+ "invoke": {
155
+ "type": "script",
156
+ "path": "scripts/tf-hook-set.js"
157
+ }
158
+ },
159
+ {
160
+ "id": "tf_list",
161
+ "name": "tf_list",
162
+ "summary": "List register tools, upstream MCPs, or hooks with their active state.",
163
+ "category": "management",
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.",
165
+ "invoke": {
166
+ "type": "script",
167
+ "path": "scripts/tf-list.js"
168
+ }
169
+ },
170
+ {
171
+ "id": "tf_log",
172
+ "name": "tf_log",
173
+ "summary": "Enable, disable, or check the gateway activity/audit log.",
174
+ "category": "management",
175
+ "instructions": "Args: { action:'enable'|'disable'|'status', path? }. Toggles ToolFunnel's JSONL audit log (tool runs + gate allow/deny decisions). Default OFF. 'status' returns {enabled,path} + recent count. Like any tool, tf_log can itself be disabled to revoke logging control.",
176
+ "invoke": {
177
+ "type": "script",
178
+ "path": "scripts/tf-log.js"
179
+ }
180
+ },
181
+ {
182
+ "id": "tf_pack",
183
+ "name": "tf_pack",
184
+ "summary": "Package the LIVE setup into a deployment-ready artifact under <home>/dist/ (portable config home, or a publishable npm package wrapping toolfunnel).",
185
+ "category": "management",
186
+ "instructions": "Args: { format?:'home'|'npm' (default 'home'), name?, version?, description?, out?, force? }. Snapshots the current setup - tools + scripts, hot/hidden curation (tools.state.json), upstream references (mcp/expose.json), policy hooks (THE GATE TRAVELS), and identity (toolfunnel.json, rewritten with the pack's name/version; a declared `requires` travels too) - into <home>/dist/<out||name>/, a SEPARATE location from the live config. format 'home' = a portable config home (zip / git init / run via --config-dir). format 'npm' = a publishable package: generated package.json with toolfunnel as a caret DEPENDENCY (depend, never copy), a 2-line bin launcher pointing --config-dir at the bundled home/, and a README stub - then `cd dist/<name> && npm publish`. NEVER ships auth/ or logs/. Refuses a non-empty destination without force:true. out is a folder NAME under dist/, never a path.",
187
+ "inputSchema": {
188
+ "type": "object",
189
+ "properties": {
190
+ "format": {
191
+ "type": "string",
192
+ "enum": [
193
+ "home",
194
+ "npm"
195
+ ],
196
+ "description": "Artifact type: portable config home (default) or publishable npm package."
197
+ },
198
+ "name": {
199
+ "type": "string",
200
+ "description": "Pack/package name (default: the home's serverName)."
201
+ },
202
+ "version": {
203
+ "type": "string",
204
+ "description": "Pack version (default: the home's serverVersion, else 0.1.0)."
205
+ },
206
+ "description": {
207
+ "type": "string",
208
+ "description": "npm format: the package.json description."
209
+ },
210
+ "out": {
211
+ "type": "string",
212
+ "description": "Folder NAME under <home>/dist/ (default: name)."
213
+ },
214
+ "force": {
215
+ "type": "boolean",
216
+ "description": "Overwrite files in an existing non-empty destination."
217
+ }
218
+ },
219
+ "additionalProperties": false
220
+ },
221
+ "invoke": {
222
+ "type": "script",
223
+ "path": "scripts/tf-pack.js"
224
+ }
225
+ }
226
+ ]
227
+ }
@@ -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 };