toolfunnel 0.4.0 → 0.6.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 +102 -213
- package/bin/toolfunnel.js +372 -147
- package/docs/KNOWN_BUGS.md +99 -225
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +70 -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 +31 -31
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -0
- package/mcp/servers/mock-upstream/server.js +192 -12
- package/package.json +1 -1
- package/src/auth/config.js +174 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +344 -344
- package/src/core/config-home.js +134 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +275 -275
- package/src/core/hook-loader.js +791 -791
- package/src/core/hook-runner.js +346 -346
- package/src/core/logger.js +171 -171
- package/src/core/matcher.js +51 -51
- package/src/core/metrics.js +75 -75
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -77
- package/src/core/tool-registry.js +180 -180
- package/src/extend/add-hook.md +152 -142
- package/src/extend/add-mcp.md +122 -121
- package/src/extend/configure.md +87 -0
- package/src/extend/create-tool.md +168 -152
- package/src/extend/howto.js +102 -98
- package/src/extend/package.md +187 -187
- package/src/extend/wrap.md +99 -0
- package/src/mcp/aggregator.js +1246 -807
- package/src/mcp/expose-store.js +460 -430
- package/src/mcp/gated-run.js +201 -194
- package/src/mcp/http-transport.js +1311 -981
- package/src/mcp/mcp-client.js +907 -530
- package/src/mcp/modern.js +372 -0
- package/src/mcp/protocol.js +468 -466
- package/src/mcp/server.js +2688 -1322
- package/src/packages/packager.js +715 -715
- package/src/tools/drift.js +138 -138
- package/src/tools/registry.js +513 -511
- package/src/tools/tool-state.js +170 -104
- package/src/ui/public/app.js +1174 -1055
- package/src/ui/public/index.html +286 -249
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1348 -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 +112 -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 +227 -216
- package/docs/admin-ui.md +0 -404
- package/docs/configuration.md +0 -402
- package/docs/hooks-and-gating.md +0 -428
- package/docs/logging.md +0 -223
- package/docs/management.md +0 -429
- package/docs/oauth.md +0 -103
- package/docs/releasing.md +0 -31
- package/docs/setup.md +0 -352
- package/docs/tools-and-upstreams.md +0 -469
package/tools/scripts/base64.js
CHANGED
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* base64-tool.js
|
|
6
|
-
*
|
|
7
|
-
* Purpose
|
|
8
|
-
* -------
|
|
9
|
-
* Encode UTF-8 text to standard Base64, or decode Base64 back to UTF-8 text.
|
|
10
|
-
* A genuinely useful utility for the Tool Manager: inspecting / producing
|
|
11
|
-
* Base64 blobs is a frequent chore and this gives a gated, no-side-effect way
|
|
12
|
-
* to do it. Uses ONLY Node built-ins (Buffer)
|
|
13
|
-
*
|
|
14
|
-
* Contract with the register (the register's `defaultRunScript`)
|
|
15
|
-
* ----------------------------------------------------------
|
|
16
|
-
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
17
|
-
* - Args: { op: "encode" | "decode", text: <string> }.
|
|
18
|
-
* - Prints a SINGLE JSON object to stdout and exits 0
|
|
19
|
-
* on bad input. Bad input is reported as { ok:false, error } rather than a
|
|
20
|
-
* thrown exception, because the gated run-path treats exit-0-with-parseable
|
|
21
|
-
* stdout as a completed run.
|
|
22
|
-
*
|
|
23
|
-
* Output (stdout), exactly one JSON object:
|
|
24
|
-
* success: { ok:true, op, result }
|
|
25
|
-
* failure: { ok:false, error }
|
|
26
|
-
*
|
|
27
|
-
* Notes on tolerance
|
|
28
|
-
* ------------------
|
|
29
|
-
* - encode: any string is encodable. A non-string `text` is rejected.
|
|
30
|
-
* - decode: Node's Buffer.from(x,'base64') is itself lenient (it skips
|
|
31
|
-
* non-base64 chars), so to give callers a MEANINGFUL "bad base64" signal we
|
|
32
|
-
* validate the input shape first and round-trip-check the result. If the
|
|
33
|
-
* re-encoded form does not match a normalised version of the input, we
|
|
34
|
-
* report ok:false rather than silently returning garbage.
|
|
35
|
-
*
|
|
36
|
-
* Safety invariants (mirror the isolation rule):
|
|
37
|
-
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
38
|
-
* - NEVER throws for ordinary bad input
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
43
|
-
* Returns the parsed value, or a sentinel describing a parse failure so the
|
|
44
|
-
* caller can surface a clean { ok:false, error } instead of crashing.
|
|
45
|
-
*
|
|
46
|
-
* @returns {{ value: any } | { parseError: string }}
|
|
47
|
-
*/
|
|
48
|
-
function parseStructuredArgs() {
|
|
49
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
50
|
-
if (raw === undefined || raw === null || raw === '') {
|
|
51
|
-
return { value: null };
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
return { value: JSON.parse(raw) };
|
|
55
|
-
} catch (_err) {
|
|
56
|
-
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Decode tolerantly: validate that the input looks like Base64, decode it, and
|
|
62
|
-
* round-trip-check so genuinely malformed input is reported rather than mangled.
|
|
63
|
-
*
|
|
64
|
-
* @param {string} text the candidate Base64 string
|
|
65
|
-
* @returns {{ ok:true, result:string } | { ok:false, error:string }}
|
|
66
|
-
*/
|
|
67
|
-
function decodeTolerant(text) {
|
|
68
|
-
// Strip surrounding whitespace; Base64 may legitimately contain internal
|
|
69
|
-
// newlines (MIME wrapping), so remove all ASCII whitespace before validating.
|
|
70
|
-
const stripped = String(text).replace(/\s+/g, '');
|
|
71
|
-
if (stripped.length === 0) {
|
|
72
|
-
// Empty input decodes to empty string
|
|
73
|
-
return { ok: true, result: '' };
|
|
74
|
-
}
|
|
75
|
-
// Standard Base64 alphabet (with optional '=' padding). Reject anything else
|
|
76
|
-
// up front so the caller gets a clear "bad base64" rather than silent garbage.
|
|
77
|
-
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(stripped)) {
|
|
78
|
-
return { ok: false, error: 'input is not valid Base64' };
|
|
79
|
-
}
|
|
80
|
-
// Length of a Base64 string (sans padding-trimming nuance) must be a multiple
|
|
81
|
-
// of 4 once padding is included; reject obviously truncated input.
|
|
82
|
-
if (stripped.length % 4 !== 0) {
|
|
83
|
-
return { ok: false, error: 'input is not valid Base64 (bad length)' };
|
|
84
|
-
}
|
|
85
|
-
const buf = Buffer.from(stripped, 'base64');
|
|
86
|
-
// Round-trip: re-encode and compare to the normalised input. Buffer's decoder
|
|
87
|
-
// is lenient, so this catches inputs that "decode" but were never valid.
|
|
88
|
-
const reencoded = buf.toString('base64');
|
|
89
|
-
if (reencoded !== stripped) {
|
|
90
|
-
return { ok: false, error: 'input is not valid Base64' };
|
|
91
|
-
}
|
|
92
|
-
return { ok: true, result: buf.toString('utf8') };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
97
|
-
*
|
|
98
|
-
* @param {any} args
|
|
99
|
-
* @returns {object} the JSON-serialisable result
|
|
100
|
-
*/
|
|
101
|
-
function run(args) {
|
|
102
|
-
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
103
|
-
return { ok: false, error: 'args must be an object { op, text }' };
|
|
104
|
-
}
|
|
105
|
-
const { op, text } = args;
|
|
106
|
-
if (op !== 'encode' && op !== 'decode') {
|
|
107
|
-
return { ok: false, error: 'op must be "encode" or "decode"' };
|
|
108
|
-
}
|
|
109
|
-
if (typeof text !== 'string') {
|
|
110
|
-
return { ok: false, error: 'text must be a string' };
|
|
111
|
-
}
|
|
112
|
-
if (op === 'encode') {
|
|
113
|
-
return { ok: true, op: 'encode', result: Buffer.from(text, 'utf8').toString('base64') };
|
|
114
|
-
}
|
|
115
|
-
// op === 'decode'
|
|
116
|
-
const decoded = decodeTolerant(text);
|
|
117
|
-
if (!decoded.ok) return { ok: false, error: decoded.error };
|
|
118
|
-
return { ok: true, op: 'decode', result: decoded.result };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function main() {
|
|
122
|
-
const parsed = parseStructuredArgs();
|
|
123
|
-
let payload;
|
|
124
|
-
if (parsed.parseError) {
|
|
125
|
-
payload = { ok: false, error: parsed.parseError };
|
|
126
|
-
} else {
|
|
127
|
-
try {
|
|
128
|
-
payload = run(parsed.value);
|
|
129
|
-
} catch (err) {
|
|
130
|
-
// Defensive: run() is written not to throw, but never let an unexpected
|
|
131
|
-
// error escape as a non-zero exit / unparseable stdout.
|
|
132
|
-
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
136
|
-
process.exit(0);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* base64-tool.js - a safe, dependency-free local Base64 codec tool.
|
|
6
|
+
*
|
|
7
|
+
* Purpose
|
|
8
|
+
* -------
|
|
9
|
+
* Encode UTF-8 text to standard Base64, or decode Base64 back to UTF-8 text.
|
|
10
|
+
* A genuinely useful utility for the Tool Manager: inspecting / producing
|
|
11
|
+
* Base64 blobs is a frequent chore and this gives a gated, no-side-effect way
|
|
12
|
+
* to do it. Uses ONLY Node built-ins (Buffer) - no network, no filesystem.
|
|
13
|
+
*
|
|
14
|
+
* Contract with the register (the register's `defaultRunScript`)
|
|
15
|
+
* ----------------------------------------------------------
|
|
16
|
+
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
17
|
+
* - Args: { op: "encode" | "decode", text: <string> }.
|
|
18
|
+
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
|
|
19
|
+
* on bad input. Bad input is reported as { ok:false, error } rather than a
|
|
20
|
+
* thrown exception, because the gated run-path treats exit-0-with-parseable
|
|
21
|
+
* stdout as a completed run.
|
|
22
|
+
*
|
|
23
|
+
* Output (stdout), exactly one JSON object:
|
|
24
|
+
* success: { ok:true, op, result }
|
|
25
|
+
* failure: { ok:false, error }
|
|
26
|
+
*
|
|
27
|
+
* Notes on tolerance
|
|
28
|
+
* ------------------
|
|
29
|
+
* - encode: any string is encodable. A non-string `text` is rejected.
|
|
30
|
+
* - decode: Node's Buffer.from(x,'base64') is itself lenient (it skips
|
|
31
|
+
* non-base64 chars), so to give callers a MEANINGFUL "bad base64" signal we
|
|
32
|
+
* validate the input shape first and round-trip-check the result. If the
|
|
33
|
+
* re-encoded form does not match a normalised version of the input, we
|
|
34
|
+
* report ok:false rather than silently returning garbage.
|
|
35
|
+
*
|
|
36
|
+
* Safety invariants (mirror the isolation rule):
|
|
37
|
+
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
38
|
+
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
43
|
+
* Returns the parsed value, or a sentinel describing a parse failure so the
|
|
44
|
+
* caller can surface a clean { ok:false, error } instead of crashing.
|
|
45
|
+
*
|
|
46
|
+
* @returns {{ value: any } | { parseError: string }}
|
|
47
|
+
*/
|
|
48
|
+
function parseStructuredArgs() {
|
|
49
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
50
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
51
|
+
return { value: null };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return { value: JSON.parse(raw) };
|
|
55
|
+
} catch (_err) {
|
|
56
|
+
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Decode tolerantly: validate that the input looks like Base64, decode it, and
|
|
62
|
+
* round-trip-check so genuinely malformed input is reported rather than mangled.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} text the candidate Base64 string
|
|
65
|
+
* @returns {{ ok:true, result:string } | { ok:false, error:string }}
|
|
66
|
+
*/
|
|
67
|
+
function decodeTolerant(text) {
|
|
68
|
+
// Strip surrounding whitespace; Base64 may legitimately contain internal
|
|
69
|
+
// newlines (MIME wrapping), so remove all ASCII whitespace before validating.
|
|
70
|
+
const stripped = String(text).replace(/\s+/g, '');
|
|
71
|
+
if (stripped.length === 0) {
|
|
72
|
+
// Empty input decodes to empty string - a valid, unambiguous result.
|
|
73
|
+
return { ok: true, result: '' };
|
|
74
|
+
}
|
|
75
|
+
// Standard Base64 alphabet (with optional '=' padding). Reject anything else
|
|
76
|
+
// up front so the caller gets a clear "bad base64" rather than silent garbage.
|
|
77
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(stripped)) {
|
|
78
|
+
return { ok: false, error: 'input is not valid Base64' };
|
|
79
|
+
}
|
|
80
|
+
// Length of a Base64 string (sans padding-trimming nuance) must be a multiple
|
|
81
|
+
// of 4 once padding is included; reject obviously truncated input.
|
|
82
|
+
if (stripped.length % 4 !== 0) {
|
|
83
|
+
return { ok: false, error: 'input is not valid Base64 (bad length)' };
|
|
84
|
+
}
|
|
85
|
+
const buf = Buffer.from(stripped, 'base64');
|
|
86
|
+
// Round-trip: re-encode and compare to the normalised input. Buffer's decoder
|
|
87
|
+
// is lenient, so this catches inputs that "decode" but were never valid.
|
|
88
|
+
const reencoded = buf.toString('base64');
|
|
89
|
+
if (reencoded !== stripped) {
|
|
90
|
+
return { ok: false, error: 'input is not valid Base64' };
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, result: buf.toString('utf8') };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
97
|
+
*
|
|
98
|
+
* @param {any} args
|
|
99
|
+
* @returns {object} the JSON-serialisable result
|
|
100
|
+
*/
|
|
101
|
+
function run(args) {
|
|
102
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
103
|
+
return { ok: false, error: 'args must be an object { op, text }' };
|
|
104
|
+
}
|
|
105
|
+
const { op, text } = args;
|
|
106
|
+
if (op !== 'encode' && op !== 'decode') {
|
|
107
|
+
return { ok: false, error: 'op must be "encode" or "decode"' };
|
|
108
|
+
}
|
|
109
|
+
if (typeof text !== 'string') {
|
|
110
|
+
return { ok: false, error: 'text must be a string' };
|
|
111
|
+
}
|
|
112
|
+
if (op === 'encode') {
|
|
113
|
+
return { ok: true, op: 'encode', result: Buffer.from(text, 'utf8').toString('base64') };
|
|
114
|
+
}
|
|
115
|
+
// op === 'decode'
|
|
116
|
+
const decoded = decodeTolerant(text);
|
|
117
|
+
if (!decoded.ok) return { ok: false, error: decoded.error };
|
|
118
|
+
return { ok: true, op: 'decode', result: decoded.result };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function main() {
|
|
122
|
+
const parsed = parseStructuredArgs();
|
|
123
|
+
let payload;
|
|
124
|
+
if (parsed.parseError) {
|
|
125
|
+
payload = { ok: false, error: parsed.parseError };
|
|
126
|
+
} else {
|
|
127
|
+
try {
|
|
128
|
+
payload = run(parsed.value);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// Defensive: run() is written not to throw, but never let an unexpected
|
|
131
|
+
// error escape as a non-zero exit / unparseable stdout.
|
|
132
|
+
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main();
|
package/tools/scripts/danger.js
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* danger-tool.js
|
|
6
|
-
*
|
|
7
|
-
* The safety case is: `toolfunnel_run_tool` must route through the hook
|
|
8
|
-
* engine's PreToolUse gate, and a DENY there must mean the tool's side effect
|
|
9
|
-
* never happens.
|
|
10
|
-
*
|
|
11
|
-
* This tool's observable side effect is: it APPENDS one line to the file named
|
|
12
|
-
* by the env var TOOLFUNNEL_DANGER_LOG. A test arranges a PreToolUse hook that
|
|
13
|
-
* DENIES this tool, runs the gated path, and then asserts the log file was NOT
|
|
14
|
-
* written (or did not gain a line). If the gate had failed open, the line would
|
|
15
|
-
* appear.
|
|
16
|
-
*
|
|
17
|
-
* When it DOES run (gate allows, or run-direct), it:
|
|
18
|
-
* 1. appends `<ISO timestamp> danger-tool fired <args>\n` to TOOLFUNNEL_DANGER_LOG
|
|
19
|
-
* 2. prints a JSON confirmation to stdout
|
|
20
|
-
* 3. exits 0
|
|
21
|
-
*
|
|
22
|
-
* If TOOLFUNNEL_DANGER_LOG is unset, it still "fires" (prints, exits 0) but
|
|
23
|
-
* writes nothing
|
|
24
|
-
* effect and the caller chooses the path.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const fs = require('node:fs');
|
|
28
|
-
|
|
29
|
-
function structuredArgs() {
|
|
30
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
31
|
-
if (!raw) return null;
|
|
32
|
-
try {
|
|
33
|
-
return JSON.parse(raw);
|
|
34
|
-
} catch (_err) {
|
|
35
|
-
return { _parseError: raw };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function main() {
|
|
40
|
-
const logPath = process.env.TOOLFUNNEL_DANGER_LOG;
|
|
41
|
-
const args = structuredArgs();
|
|
42
|
-
const argv = process.argv.slice(2);
|
|
43
|
-
let wrote = false;
|
|
44
|
-
|
|
45
|
-
if (logPath) {
|
|
46
|
-
const line =
|
|
47
|
-
new Date().toISOString() +
|
|
48
|
-
' danger-tool fired ' +
|
|
49
|
-
JSON.stringify({ args, argv }) +
|
|
50
|
-
'\n';
|
|
51
|
-
fs.appendFileSync(logPath, line, 'utf8');
|
|
52
|
-
wrote = true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
process.stdout.write(
|
|
56
|
-
JSON.stringify({ ok: true, fired: true, wrote, logPath: logPath || null }) + '\n'
|
|
57
|
-
);
|
|
58
|
-
process.exit(0);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* danger-tool.js - a demo "dangerous" tool used to PROVE the gate works.
|
|
6
|
+
*
|
|
7
|
+
* The safety case is: `toolfunnel_run_tool` must route through the hook
|
|
8
|
+
* engine's PreToolUse gate, and a DENY there must mean the tool's side effect
|
|
9
|
+
* never happens.
|
|
10
|
+
*
|
|
11
|
+
* This tool's observable side effect is: it APPENDS one line to the file named
|
|
12
|
+
* by the env var TOOLFUNNEL_DANGER_LOG. A test arranges a PreToolUse hook that
|
|
13
|
+
* DENIES this tool, runs the gated path, and then asserts the log file was NOT
|
|
14
|
+
* written (or did not gain a line). If the gate had failed open, the line would
|
|
15
|
+
* appear.
|
|
16
|
+
*
|
|
17
|
+
* When it DOES run (gate allows, or run-direct), it:
|
|
18
|
+
* 1. appends `<ISO timestamp> danger-tool fired <args>\n` to TOOLFUNNEL_DANGER_LOG
|
|
19
|
+
* 2. prints a JSON confirmation to stdout
|
|
20
|
+
* 3. exits 0
|
|
21
|
+
*
|
|
22
|
+
* If TOOLFUNNEL_DANGER_LOG is unset, it still "fires" (prints, exits 0) but
|
|
23
|
+
* writes nothing - so it is never destructive on its own; the log is the only
|
|
24
|
+
* effect and the caller chooses the path.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
|
|
29
|
+
function structuredArgs() {
|
|
30
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
31
|
+
if (!raw) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
} catch (_err) {
|
|
35
|
+
return { _parseError: raw };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function main() {
|
|
40
|
+
const logPath = process.env.TOOLFUNNEL_DANGER_LOG;
|
|
41
|
+
const args = structuredArgs();
|
|
42
|
+
const argv = process.argv.slice(2);
|
|
43
|
+
let wrote = false;
|
|
44
|
+
|
|
45
|
+
if (logPath) {
|
|
46
|
+
const line =
|
|
47
|
+
new Date().toISOString() +
|
|
48
|
+
' danger-tool fired ' +
|
|
49
|
+
JSON.stringify({ args, argv }) +
|
|
50
|
+
'\n';
|
|
51
|
+
fs.appendFileSync(logPath, line, 'utf8');
|
|
52
|
+
wrote = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
process.stdout.write(
|
|
56
|
+
JSON.stringify({ ok: true, fired: true, wrote, logPath: logPath || null }) + '\n'
|
|
57
|
+
);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main();
|
package/tools/scripts/echo.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* echo-tool.js
|
|
6
|
-
*
|
|
7
|
-
* Prints a JSON object of the args it was invoked with, then exits 0.
|
|
8
|
-
* It has NO side effects: it touches no filesystem, no network, no env-driven
|
|
9
|
-
* writes. It exists so the register / run-path / gate tests have a benign tool
|
|
10
|
-
* to exercise (the "allowed" counterpart to danger-tool.js).
|
|
11
|
-
*
|
|
12
|
-
* Args are read two ways so callers can use whichever is convenient:
|
|
13
|
-
* 1. process.argv
|
|
14
|
-
* 2. TOOLFUNNEL_TOOL_ARGS
|
|
15
|
-
* (this is how the register's "script" invoke passes `args` through).
|
|
16
|
-
*
|
|
17
|
-
* Output (stdout): a single JSON object { ok, args, argv }.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
function parseStructuredArgs() {
|
|
21
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
22
|
-
if (!raw) return null;
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(raw);
|
|
25
|
-
} catch (_err) {
|
|
26
|
-
// Malformed env
|
|
27
|
-
return { _parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${raw}` };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function main() {
|
|
32
|
-
const argv = process.argv.slice(2);
|
|
33
|
-
const args = parseStructuredArgs();
|
|
34
|
-
const payload = { ok: true, args, argv };
|
|
35
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
36
|
-
process.exit(0);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* echo-tool.js - a safe local demo tool.
|
|
6
|
+
*
|
|
7
|
+
* Prints a JSON object of the args it was invoked with, then exits 0.
|
|
8
|
+
* It has NO side effects: it touches no filesystem, no network, no env-driven
|
|
9
|
+
* writes. It exists so the register / run-path / gate tests have a benign tool
|
|
10
|
+
* to exercise (the "allowed" counterpart to danger-tool.js).
|
|
11
|
+
*
|
|
12
|
+
* Args are read two ways so callers can use whichever is convenient:
|
|
13
|
+
* 1. process.argv - positional CLI args, e.g. `node echo-tool.js a b c`
|
|
14
|
+
* 2. TOOLFUNNEL_TOOL_ARGS - a JSON-encoded object/array of structured args
|
|
15
|
+
* (this is how the register's "script" invoke passes `args` through).
|
|
16
|
+
*
|
|
17
|
+
* Output (stdout): a single JSON object { ok, args, argv }.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function parseStructuredArgs() {
|
|
21
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
22
|
+
if (!raw) return null;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
} catch (_err) {
|
|
26
|
+
// Malformed env - surface it rather than silently dropping.
|
|
27
|
+
return { _parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${raw}` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function main() {
|
|
32
|
+
const argv = process.argv.slice(2);
|
|
33
|
+
const args = parseStructuredArgs();
|
|
34
|
+
const payload = { ok: true, args, argv };
|
|
35
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
main();
|