toolfunnel 0.7.0 → 0.8.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.
Files changed (73) hide show
  1. package/README.md +238 -235
  2. package/SECURITY.md +84 -84
  3. package/bin/toolfunnel.js +530 -451
  4. package/docs/KNOWN_BUGS.md +100 -80
  5. package/docs/MANUAL.pdf +0 -0
  6. package/docs/design.md +84 -84
  7. package/docs/packaging.md +116 -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 -39
  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 +379 -380
  14. package/package.json +67 -67
  15. package/src/auth/config.js +172 -174
  16. package/src/auth/install.js +181 -181
  17. package/src/auth/resource-server.js +358 -353
  18. package/src/core/atomic-write.js +97 -0
  19. package/src/core/child-reaper.js +61 -61
  20. package/src/core/config-home.js +226 -134
  21. package/src/core/events.js +105 -105
  22. package/src/core/hook-engine.js +304 -329
  23. package/src/core/hook-loader.js +766 -824
  24. package/src/core/hook-runner.js +615 -566
  25. package/src/core/logger.js +295 -297
  26. package/src/core/matcher.js +100 -100
  27. package/src/core/metrics.js +90 -75
  28. package/src/core/output-cap.js +31 -0
  29. package/src/core/rename-migration.js +178 -163
  30. package/src/core/requires.js +138 -138
  31. package/src/core/server-config.js +98 -98
  32. package/src/extend/add-hook.md +155 -152
  33. package/src/extend/add-mcp.md +119 -119
  34. package/src/extend/configure.md +88 -88
  35. package/src/extend/create-tool.md +168 -168
  36. package/src/extend/howto.js +102 -102
  37. package/src/extend/package.md +190 -190
  38. package/src/extend/wrap.md +99 -99
  39. package/src/mcp/aggregator.js +2248 -1674
  40. package/src/mcp/expose-store.js +755 -577
  41. package/src/mcp/gated-run.js +210 -210
  42. package/src/mcp/http-transport.js +1657 -1563
  43. package/src/mcp/mcp-client.js +1174 -1070
  44. package/src/mcp/modern.js +409 -406
  45. package/src/mcp/protocol.js +473 -468
  46. package/src/mcp/server.js +3317 -3112
  47. package/src/mcp/tool-approvals.js +396 -0
  48. package/src/tools/registry.js +607 -661
  49. package/src/tools/tool-state.js +170 -170
  50. package/src/ui/public/app.js +1246 -1187
  51. package/src/ui/public/index.html +286 -286
  52. package/src/ui/public/styles.css +535 -535
  53. package/src/ui/server.js +1742 -1616
  54. package/tools/scripts/base64.js +139 -139
  55. package/tools/scripts/danger.js +61 -61
  56. package/tools/scripts/echo.js +39 -39
  57. package/tools/scripts/hash.js +101 -101
  58. package/tools/scripts/json.js +109 -109
  59. package/tools/scripts/text-stats.js +131 -131
  60. package/tools/scripts/tf-accept-change.js +67 -0
  61. package/tools/scripts/tf-env.js +43 -43
  62. package/tools/scripts/tf-hook-add.js +151 -151
  63. package/tools/scripts/tf-hook-set.js +120 -120
  64. package/tools/scripts/tf-list.js +192 -192
  65. package/tools/scripts/tf-log.js +86 -86
  66. package/tools/scripts/tf-mcp-add.js +130 -117
  67. package/tools/scripts/tf-mcp-set.js +121 -121
  68. package/tools/scripts/tf-pack.js +237 -237
  69. package/tools/scripts/tf-tool-add.js +134 -134
  70. package/tools/scripts/tf-tool-set.js +142 -142
  71. package/tools/scripts/tf-wrap.js +103 -103
  72. package/tools/scripts/uuid.js +102 -102
  73. package/tools/tools.register.json +16 -5
@@ -1,192 +1,192 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * tf-list.js - a READ-ONLY management inventory tool (category "management").
6
- *
7
- * Purpose
8
- * -------
9
- * One first-party register function that answers "what is configured, and what is
10
- * its live state?" across the three stores the gateway owns:
11
- * - tools -> the persisted Tool Register + the ACTIVE/DISABLED + hidden overlay.
12
- * - mcps -> the expose store: upstream MCPs and their curated-direct exposes.
13
- * - hooks -> the hook manifest + the per-hook enabled-state overlay.
14
- *
15
- * It is discovered via list_tools and executed via run_tool through the PreToolUse
16
- * gate. It does NOT add new MCP-protocol commands; it is a "gateway-run" script
17
- * that reads the gateway's own config files. READ-ONLY: no writes, no network,
18
- * no process mutation.
19
- *
20
- * Contract with the register (registry.js `defaultRunScript`)
21
- * -----------------------------------------------------------
22
- * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
23
- * absent). The only arg is { kind: "tools" | "mcps" | "hooks" }.
24
- * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS exit 0. A bad
25
- * kind, a missing/malformed config, or any other logical error is reported as
26
- * { ok:false, error } (still exit 0), never a crash.
27
- *
28
- * Output (stdout), exactly one JSON object:
29
- * success: { ok:true, kind, items:[ ... ] }
30
- * failure: { ok:false, error }
31
- *
32
- * Per-kind item shapes:
33
- * tools -> register brief { id, name, summary, category } annotated with the
34
- * overlay-derived { enabled, hidden }.
35
- * mcps -> one item per upstream MCP (its full config incl. `enabled`) with its
36
- * curated-direct `exposed` entries nested (each carrying its resolved
37
- * downstream `name` and `enabled` flag). Uses listUpstreams() +
38
- * listExposed() and groups exposes under their owning upstream.
39
- * hooks -> one item per manifest hook (full spec) with `enabled` resolved from
40
- * the persisted overlay (readState) when present, else the manifest flag.
41
- *
42
- * Safety invariants (defense-in-depth; mirror the isolation rule):
43
- * - READ-ONLY: only reads config files INSIDE the toolfunnel root, resolved from
44
- * THIS script's own location (__dirname) - never a caller-supplied path.
45
- * - NO network, NO writes, NO process mutation.
46
- * - NEVER throws for ordinary conditions - only well-shaped JSON on stdout.
47
- */
48
-
49
- const path = require('node:path');
50
-
51
- // HOME/engine resolution shared by every tf-* script (see tf-env.js): the config lives beside
52
- // this script (the git-clone root OR a seeded external config home); the engine code is required
53
- // from the package that spawned us when it is not local.
54
- const { HOME, srcRequire } = require('./tf-env');
55
- const { loadRegistry } = srcRequire('tools/registry');
56
- const { loadToolState, isToolEnabled, isToolHidden } = srcRequire('tools/tool-state');
57
- const { loadExposeStore } = srcRequire('mcp/expose-store');
58
- const { loadManifest } = srcRequire('core/hook-loader');
59
-
60
- const ROOT = HOME;
61
-
62
- // Canonical store paths (see the script contract).
63
- const REGISTER_PATH = path.join(ROOT, 'tools', 'tools.register.json');
64
- const STATE_PATH = path.join(ROOT, 'tools', 'tools.state.json');
65
- const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
66
- const MANIFEST_PATH = path.join(ROOT, 'hooks', 'hooks.manifest.json');
67
- const SCRIPTS_ROOT = path.join(ROOT, 'tools', 'scripts');
68
-
69
- /**
70
- * Parse the structured args from env. Never throws: a missing/blank/malformed
71
- * TOOLFUNNEL_TOOL_ARGS yields {} so the kind check below produces a clean
72
- * { ok:false, error } rather than a crash.
73
- * @returns {object}
74
- */
75
- function readArgs() {
76
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
77
- if (typeof raw !== 'string' || raw.length === 0) return {};
78
- try {
79
- const parsed = JSON.parse(raw);
80
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
81
- } catch (_err) {
82
- return {};
83
- }
84
- }
85
-
86
- /**
87
- * tools -> register briefs annotated with the overlay's enabled + hidden state. HIDDEN tools are a
88
- * manager-view declutter: they are OMITTED by default and only included when args.includeHidden is
89
- * true. (This is the manager surface only - `hidden` never affects the lean list / top-level surface
90
- * the connected AI sees.)
91
- * @param {object} [args] { includeHidden?: boolean }
92
- * @returns {Array<{id,name,summary,category,enabled,hidden}>}
93
- */
94
- function listTools(args) {
95
- const includeHidden = !!(args && args.includeHidden === true);
96
- const registry = loadRegistry(REGISTER_PATH, { scriptsRoot: SCRIPTS_ROOT });
97
- const state = loadToolState(STATE_PATH);
98
- return registry.list()
99
- .map((brief) => ({
100
- ...brief,
101
- enabled: isToolEnabled(state, brief.id),
102
- hidden: isToolHidden(state, brief.id),
103
- }))
104
- .filter((t) => includeHidden || !t.hidden); // declutter: omit hidden unless explicitly asked
105
- }
106
-
107
- /**
108
- * mcps -> one item per upstream MCP, with its curated-direct exposes nested.
109
- * Uses listUpstreams() for the connection config (incl. `enabled`) and
110
- * listExposed() for the surfaced tools, grouping each expose under its owning
111
- * upstream and resolving the downstream name via exposedName().
112
- * @returns {Array<object>}
113
- */
114
- function listMcps() {
115
- const store = loadExposeStore(EXPOSE_PATH);
116
- const upstreams = store.listUpstreams();
117
- const exposed = store.listExposed();
118
-
119
- // Group exposes by their upstream id (every expose references an existing
120
- // upstream - addExpose requires it and removeUpstream cascades - but default
121
- // to an empty list so an unmatched entry never throws).
122
- const byUpstream = new Map();
123
- for (const e of exposed) {
124
- const annotated = { ...e, name: store.exposedName(e) };
125
- const bucket = byUpstream.get(e.upstream);
126
- if (bucket) bucket.push(annotated);
127
- else byUpstream.set(e.upstream, [annotated]);
128
- }
129
-
130
- return upstreams.map((u) => ({
131
- ...u,
132
- exposed: byUpstream.get(u.id) || [],
133
- }));
134
- }
135
-
136
- /**
137
- * hooks -> one item per manifest hook with `enabled` resolved from the persisted
138
- * overlay (readState) when it carries the hook id, else the manifest's own flag.
139
- * loadManifest already applies the overlay over the in-memory specs, but we read
140
- * the overlay explicitly so the resolved `enabled` is correct regardless of that.
141
- * @returns {Array<object>}
142
- */
143
- function listHooks() {
144
- const loader = loadManifest(MANIFEST_PATH);
145
- const overlay = loader.readState();
146
- return loader.hooks.map((h) => {
147
- const hasOverride = h && typeof h.id === 'string' &&
148
- Object.prototype.hasOwnProperty.call(overlay, h.id);
149
- const enabled = hasOverride ? overlay[h.id] === true : h.enabled === true;
150
- return { ...h, enabled };
151
- });
152
- }
153
-
154
- /**
155
- * Build the inventory for the requested kind. Throws on an unknown kind so main()
156
- * can surface it as { ok:false, error }.
157
- * @param {object} args
158
- * @returns {{ ok:true, kind:string, items:Array<object> }}
159
- */
160
- function build(args) {
161
- const kind = args && args.kind;
162
- let items;
163
- switch (kind) {
164
- case 'tools':
165
- items = listTools(args); // honours args.includeHidden (hidden tools omitted by default)
166
- break;
167
- case 'mcps':
168
- items = listMcps();
169
- break;
170
- case 'hooks':
171
- items = listHooks();
172
- break;
173
- default:
174
- throw new Error(`tf-list: kind must be one of "tools"|"mcps"|"hooks" (got ${JSON.stringify(kind)})`);
175
- }
176
- return { ok: true, kind, items };
177
- }
178
-
179
- function main() {
180
- let payload;
181
- try {
182
- payload = build(readArgs());
183
- } catch (err) {
184
- // Any logical error (bad kind, missing/malformed store) becomes a well-shaped
185
- // failure line. Still exit 0 - the gated runner treats parseable stdout as ok.
186
- payload = { ok: false, error: (err && err.message) || String(err) };
187
- }
188
- process.stdout.write(JSON.stringify(payload) + '\n');
189
- process.exit(0);
190
- }
191
-
192
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * tf-list.js - a READ-ONLY management inventory tool (category "management").
6
+ *
7
+ * Purpose
8
+ * -------
9
+ * One first-party register function that answers "what is configured, and what is
10
+ * its live state?" across the three stores the gateway owns:
11
+ * - tools -> the persisted Tool Register + the ACTIVE/DISABLED + hidden overlay.
12
+ * - mcps -> the expose store: upstream MCPs and their curated-direct exposes.
13
+ * - hooks -> the hook manifest + the per-hook enabled-state overlay.
14
+ *
15
+ * It is discovered via list_tools and executed via run_tool through the PreToolUse
16
+ * gate. It does NOT add new MCP-protocol commands; it is a "gateway-run" script
17
+ * that reads the gateway's own config files. READ-ONLY: no writes, no network,
18
+ * no process mutation.
19
+ *
20
+ * Contract with the register (registry.js `defaultRunScript`)
21
+ * -----------------------------------------------------------
22
+ * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
23
+ * absent). The only arg is { kind: "tools" | "mcps" | "hooks" }.
24
+ * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS exit 0. A bad
25
+ * kind, a missing/malformed config, or any other logical error is reported as
26
+ * { ok:false, error } (still exit 0), never a crash.
27
+ *
28
+ * Output (stdout), exactly one JSON object:
29
+ * success: { ok:true, kind, items:[ ... ] }
30
+ * failure: { ok:false, error }
31
+ *
32
+ * Per-kind item shapes:
33
+ * tools -> register brief { id, name, summary, category } annotated with the
34
+ * overlay-derived { enabled, hidden }.
35
+ * mcps -> one item per upstream MCP (its full config incl. `enabled`) with its
36
+ * curated-direct `exposed` entries nested (each carrying its resolved
37
+ * downstream `name` and `enabled` flag). Uses listUpstreams() +
38
+ * listExposed() and groups exposes under their owning upstream.
39
+ * hooks -> one item per manifest hook (full spec) with `enabled` resolved from
40
+ * the persisted overlay (readState) when present, else the manifest flag.
41
+ *
42
+ * Safety invariants (defense-in-depth; mirror the isolation rule):
43
+ * - READ-ONLY: only reads config files INSIDE the toolfunnel root, resolved from
44
+ * THIS script's own location (__dirname) - never a caller-supplied path.
45
+ * - NO network, NO writes, NO process mutation.
46
+ * - NEVER throws for ordinary conditions - only well-shaped JSON on stdout.
47
+ */
48
+
49
+ const path = require('node:path');
50
+
51
+ // HOME/engine resolution shared by every tf-* script (see tf-env.js): the config lives beside
52
+ // this script (the git-clone root OR a seeded external config home); the engine code is required
53
+ // from the package that spawned us when it is not local.
54
+ const { HOME, srcRequire } = require('./tf-env');
55
+ const { loadRegistry } = srcRequire('tools/registry');
56
+ const { loadToolState, isToolEnabled, isToolHidden } = srcRequire('tools/tool-state');
57
+ const { loadExposeStore } = srcRequire('mcp/expose-store');
58
+ const { loadManifest } = srcRequire('core/hook-loader');
59
+
60
+ const ROOT = HOME;
61
+
62
+ // Canonical store paths (see the script contract).
63
+ const REGISTER_PATH = path.join(ROOT, 'tools', 'tools.register.json');
64
+ const STATE_PATH = path.join(ROOT, 'tools', 'tools.state.json');
65
+ const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
66
+ const MANIFEST_PATH = path.join(ROOT, 'hooks', 'hooks.manifest.json');
67
+ const SCRIPTS_ROOT = path.join(ROOT, 'tools', 'scripts');
68
+
69
+ /**
70
+ * Parse the structured args from env. Never throws: a missing/blank/malformed
71
+ * TOOLFUNNEL_TOOL_ARGS yields {} so the kind check below produces a clean
72
+ * { ok:false, error } rather than a crash.
73
+ * @returns {object}
74
+ */
75
+ function readArgs() {
76
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
77
+ if (typeof raw !== 'string' || raw.length === 0) return {};
78
+ try {
79
+ const parsed = JSON.parse(raw);
80
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
81
+ } catch (_err) {
82
+ return {};
83
+ }
84
+ }
85
+
86
+ /**
87
+ * tools -> register briefs annotated with the overlay's enabled + hidden state. HIDDEN tools are a
88
+ * manager-view declutter: they are OMITTED by default and only included when args.includeHidden is
89
+ * true. (This is the manager surface only - `hidden` never affects the lean list / top-level surface
90
+ * the connected AI sees.)
91
+ * @param {object} [args] { includeHidden?: boolean }
92
+ * @returns {Array<{id,name,summary,category,enabled,hidden}>}
93
+ */
94
+ function listTools(args) {
95
+ const includeHidden = !!(args && args.includeHidden === true);
96
+ const registry = loadRegistry(REGISTER_PATH, { scriptsRoot: SCRIPTS_ROOT });
97
+ const state = loadToolState(STATE_PATH);
98
+ return registry.list()
99
+ .map((brief) => ({
100
+ ...brief,
101
+ enabled: isToolEnabled(state, brief.id),
102
+ hidden: isToolHidden(state, brief.id),
103
+ }))
104
+ .filter((t) => includeHidden || !t.hidden); // declutter: omit hidden unless explicitly asked
105
+ }
106
+
107
+ /**
108
+ * mcps -> one item per upstream MCP, with its curated-direct exposes nested.
109
+ * Uses listUpstreams() for the connection config (incl. `enabled`) and
110
+ * listExposed() for the surfaced tools, grouping each expose under its owning
111
+ * upstream and resolving the downstream name via exposedName().
112
+ * @returns {Array<object>}
113
+ */
114
+ function listMcps() {
115
+ const store = loadExposeStore(EXPOSE_PATH);
116
+ const upstreams = store.listUpstreams();
117
+ const exposed = store.listExposed();
118
+
119
+ // Group exposes by their upstream id (every expose references an existing
120
+ // upstream - addExpose requires it and removeUpstream cascades - but default
121
+ // to an empty list so an unmatched entry never throws).
122
+ const byUpstream = new Map();
123
+ for (const e of exposed) {
124
+ const annotated = { ...e, name: store.exposedName(e) };
125
+ const bucket = byUpstream.get(e.upstream);
126
+ if (bucket) bucket.push(annotated);
127
+ else byUpstream.set(e.upstream, [annotated]);
128
+ }
129
+
130
+ return upstreams.map((u) => ({
131
+ ...u,
132
+ exposed: byUpstream.get(u.id) || [],
133
+ }));
134
+ }
135
+
136
+ /**
137
+ * hooks -> one item per manifest hook with `enabled` resolved from the persisted
138
+ * overlay (readState) when it carries the hook id, else the manifest's own flag.
139
+ * loadManifest already applies the overlay over the in-memory specs, but we read
140
+ * the overlay explicitly so the resolved `enabled` is correct regardless of that.
141
+ * @returns {Array<object>}
142
+ */
143
+ function listHooks() {
144
+ const loader = loadManifest(MANIFEST_PATH);
145
+ const overlay = loader.readState();
146
+ return loader.hooks.map((h) => {
147
+ const hasOverride = h && typeof h.id === 'string' &&
148
+ Object.prototype.hasOwnProperty.call(overlay, h.id);
149
+ const enabled = hasOverride ? overlay[h.id] === true : h.enabled === true;
150
+ return { ...h, enabled };
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Build the inventory for the requested kind. Throws on an unknown kind so main()
156
+ * can surface it as { ok:false, error }.
157
+ * @param {object} args
158
+ * @returns {{ ok:true, kind:string, items:Array<object> }}
159
+ */
160
+ function build(args) {
161
+ const kind = args && args.kind;
162
+ let items;
163
+ switch (kind) {
164
+ case 'tools':
165
+ items = listTools(args); // honours args.includeHidden (hidden tools omitted by default)
166
+ break;
167
+ case 'mcps':
168
+ items = listMcps();
169
+ break;
170
+ case 'hooks':
171
+ items = listHooks();
172
+ break;
173
+ default:
174
+ throw new Error(`tf-list: kind must be one of "tools"|"mcps"|"hooks" (got ${JSON.stringify(kind)})`);
175
+ }
176
+ return { ok: true, kind, items };
177
+ }
178
+
179
+ function main() {
180
+ let payload;
181
+ try {
182
+ payload = build(readArgs());
183
+ } catch (err) {
184
+ // Any logical error (bad kind, missing/malformed store) becomes a well-shaped
185
+ // failure line. Still exit 0 - the gated runner treats parseable stdout as ok.
186
+ payload = { ok: false, error: (err && err.message) || String(err) };
187
+ }
188
+ process.stdout.write(JSON.stringify(payload) + '\n');
189
+ process.exit(0);
190
+ }
191
+
192
+ main();
@@ -1,86 +1,86 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * tf-log.js - gateway-run MANAGEMENT tool: enable, disable, or check the activity log.
6
- *
7
- * A first-party "management" script-tool (discovered via list_tools, executed via
8
- * run_tool through the PreToolUse gate). It toggles ToolFunnel's own JSONL audit log
9
- * (tool runs + gate allow/deny decisions) via src/core/logger. Logging is DEFAULT OFF.
10
- *
11
- * Contract with the register (`defaultRunScript`)
12
- * ----------------------------------------------------------
13
- * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
14
- * absent). Shape: { action: 'enable'|'disable'|'status', path? }.
15
- * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical failure
16
- * (bad args) is reported as { ok:false, error }.
17
- *
18
- * Output (stdout), exactly one JSON object:
19
- * enable/disable: { ok:true, action, enabled, path }
20
- * status: { ok:true, action, enabled, path, count } // count = log entries
21
- * failure: { ok:false, error }
22
- */
23
-
24
- // Shared HOME/engine resolution (see tf-env.js): the logger module itself resolves the config
25
- // home from the env the gateway set, so requiring it from the right PACKAGE is all we need.
26
- const { srcRequire } = require('./tf-env');
27
- const logger = srcRequire('core/logger');
28
-
29
- /**
30
- * Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed JSON
31
- * or a non-object payload so the caller reports { ok:false, error }.
32
- * @returns {object}
33
- */
34
- function parseArgs() {
35
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
36
- if (raw === undefined || raw === null || raw === '') return {};
37
- const parsed = JSON.parse(raw);
38
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
39
- throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
40
- }
41
- return parsed;
42
- }
43
-
44
- /**
45
- * Perform the action. May throw - main() turns any throw into { ok:false, error }.
46
- * @returns {object} the success payload
47
- */
48
- function run() {
49
- const args = parseArgs();
50
- const action = args.action;
51
-
52
- switch (action) {
53
- case 'enable': {
54
- const patch = { enabled: true };
55
- if (typeof args.path === 'string' && args.path.length > 0) patch.path = args.path;
56
- const cfg = logger.setConfig(patch);
57
- return { ok: true, action, enabled: cfg.enabled, path: cfg.path };
58
- }
59
- case 'disable': {
60
- const cfg = logger.setConfig({ enabled: false });
61
- return { ok: true, action, enabled: cfg.enabled, path: cfg.path };
62
- }
63
- case 'status': {
64
- const cfg = logger.getConfig();
65
- const entries = logger.tail();
66
- return { ok: true, action, enabled: cfg.enabled, path: cfg.path, count: entries.length };
67
- }
68
- default:
69
- throw new Error(
70
- `args.action must be one of enable|disable|status (got ${JSON.stringify(action)})`
71
- );
72
- }
73
- }
74
-
75
- function main() {
76
- let payload;
77
- try {
78
- payload = run();
79
- } catch (err) {
80
- payload = { ok: false, error: (err && err.message) || String(err) };
81
- }
82
- process.stdout.write(JSON.stringify(payload) + '\n');
83
- process.exit(0);
84
- }
85
-
86
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * tf-log.js - gateway-run MANAGEMENT tool: enable, disable, or check the activity log.
6
+ *
7
+ * A first-party "management" script-tool (discovered via list_tools, executed via
8
+ * run_tool through the PreToolUse gate). It toggles ToolFunnel's own JSONL audit log
9
+ * (tool runs + gate allow/deny decisions) via src/core/logger. Logging is DEFAULT OFF.
10
+ *
11
+ * Contract with the register (`defaultRunScript`)
12
+ * ----------------------------------------------------------
13
+ * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
14
+ * absent). Shape: { action: 'enable'|'disable'|'status', path? }.
15
+ * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical failure
16
+ * (bad args) is reported as { ok:false, error }.
17
+ *
18
+ * Output (stdout), exactly one JSON object:
19
+ * enable/disable: { ok:true, action, enabled, path }
20
+ * status: { ok:true, action, enabled, path, count } // count = log entries
21
+ * failure: { ok:false, error }
22
+ */
23
+
24
+ // Shared HOME/engine resolution (see tf-env.js): the logger module itself resolves the config
25
+ // home from the env the gateway set, so requiring it from the right PACKAGE is all we need.
26
+ const { srcRequire } = require('./tf-env');
27
+ const logger = srcRequire('core/logger');
28
+
29
+ /**
30
+ * Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed JSON
31
+ * or a non-object payload so the caller reports { ok:false, error }.
32
+ * @returns {object}
33
+ */
34
+ function parseArgs() {
35
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
36
+ if (raw === undefined || raw === null || raw === '') return {};
37
+ const parsed = JSON.parse(raw);
38
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
39
+ throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
40
+ }
41
+ return parsed;
42
+ }
43
+
44
+ /**
45
+ * Perform the action. May throw - main() turns any throw into { ok:false, error }.
46
+ * @returns {object} the success payload
47
+ */
48
+ function run() {
49
+ const args = parseArgs();
50
+ const action = args.action;
51
+
52
+ switch (action) {
53
+ case 'enable': {
54
+ const patch = { enabled: true };
55
+ if (typeof args.path === 'string' && args.path.length > 0) patch.path = args.path;
56
+ const cfg = logger.setConfig(patch);
57
+ return { ok: true, action, enabled: cfg.enabled, path: cfg.path };
58
+ }
59
+ case 'disable': {
60
+ const cfg = logger.setConfig({ enabled: false });
61
+ return { ok: true, action, enabled: cfg.enabled, path: cfg.path };
62
+ }
63
+ case 'status': {
64
+ const cfg = logger.getConfig();
65
+ const entries = logger.tail();
66
+ return { ok: true, action, enabled: cfg.enabled, path: cfg.path, count: entries.length };
67
+ }
68
+ default:
69
+ throw new Error(
70
+ `args.action must be one of enable|disable|status (got ${JSON.stringify(action)})`
71
+ );
72
+ }
73
+ }
74
+
75
+ function main() {
76
+ let payload;
77
+ try {
78
+ payload = run();
79
+ } catch (err) {
80
+ payload = { ok: false, error: (err && err.message) || String(err) };
81
+ }
82
+ process.stdout.write(JSON.stringify(payload) + '\n');
83
+ process.exit(0);
84
+ }
85
+
86
+ main();