multicorn-shield 0.11.0 → 0.13.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/CHANGELOG.md +39 -0
- package/dist/badge.js +4 -4
- package/dist/index.cjs +38 -19
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +37 -20
- package/dist/multicorn-proxy.js +578 -15
- package/dist/openclaw-hook/handler.js +0 -1
- package/dist/openclaw-plugin/multicorn-shield.js +11 -17
- package/dist/openclaw-plugin/openclaw.plugin.json +3 -1
- package/dist/proxy.cjs +174 -0
- package/dist/proxy.d.cts +228 -1
- package/dist/proxy.d.ts +228 -1
- package/dist/proxy.js +174 -1
- package/dist/shield-extension.js +1 -4
- package/package.json +4 -2
- package/plugins/cline/README.md +61 -0
- package/plugins/cline/hooks/scripts/post-tool-use.cjs +116 -0
- package/plugins/cline/hooks/scripts/pre-tool-use.cjs +271 -0
- package/plugins/cline/hooks/scripts/shared.cjs +303 -0
- package/plugins/gemini-cli/hooks/scripts/after-tool.cjs +110 -0
- package/plugins/gemini-cli/hooks/scripts/before-tool.cjs +197 -0
- package/plugins/gemini-cli/hooks/scripts/shared.cjs +319 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
|
|
3
|
+
/**
|
|
4
|
+
* Gemini CLI BeforeTool hook: asks Shield whether a tool call is allowed.
|
|
5
|
+
* Reads JSON from stdin. Writes JSON to stdout only (decision allow/deny). Logs to stderr.
|
|
6
|
+
* Fail-open on missing config or unreachable API.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
loadConfig,
|
|
13
|
+
logPrefix,
|
|
14
|
+
mapToolName,
|
|
15
|
+
postJson,
|
|
16
|
+
readStdin,
|
|
17
|
+
safeJsonParse,
|
|
18
|
+
scrubParameters,
|
|
19
|
+
unwrapData,
|
|
20
|
+
consentUrl,
|
|
21
|
+
openBrowser,
|
|
22
|
+
} = require("./shared.cjs");
|
|
23
|
+
|
|
24
|
+
const HOOK_PREFIX = logPrefix("before-tool");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} decision
|
|
28
|
+
* @param {string} [reason]
|
|
29
|
+
*/
|
|
30
|
+
function respond(decision, reason) {
|
|
31
|
+
/** @type {Record<string, unknown>} */
|
|
32
|
+
const out = { decision };
|
|
33
|
+
if (reason !== undefined && reason.length > 0) {
|
|
34
|
+
out.reason = reason;
|
|
35
|
+
}
|
|
36
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dashboardHintUrl(apiBaseUrl) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = String(apiBaseUrl).replace(/\/+$/, "");
|
|
43
|
+
const lower = raw.toLowerCase();
|
|
44
|
+
if (lower.includes("localhost:8080") || lower.includes("127.0.0.1:8080")) {
|
|
45
|
+
return "http://localhost:5173/approvals";
|
|
46
|
+
}
|
|
47
|
+
const u = new URL(raw);
|
|
48
|
+
if (u.hostname.startsWith("api.")) {
|
|
49
|
+
u.hostname = "app." + u.hostname.slice(4);
|
|
50
|
+
}
|
|
51
|
+
return `${u.origin}/approvals`;
|
|
52
|
+
} catch {
|
|
53
|
+
return "https://app.multicorn.ai/approvals";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {unknown} data
|
|
59
|
+
* @param {string} approvalsUrl
|
|
60
|
+
*/
|
|
61
|
+
function blockedReason(data, approvalsUrl) {
|
|
62
|
+
if (data !== null && typeof data === "object") {
|
|
63
|
+
const d = /** @type {Record<string, unknown>} */ (data);
|
|
64
|
+
const meta = d.metadata;
|
|
65
|
+
if (typeof meta === "string" && meta.length > 0) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(meta);
|
|
68
|
+
if (parsed !== null && typeof parsed === "object" && "block_reason" in parsed) {
|
|
69
|
+
const br = /** @type {Record<string, unknown>} */ (parsed).block_reason;
|
|
70
|
+
if (typeof br === "string" && br.length > 0) {
|
|
71
|
+
return `Blocked by Multicorn Shield: ${br}. Grant access at ${approvalsUrl}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return `Blocked by Multicorn Shield. Grant access at ${approvalsUrl}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = await readStdin();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
88
|
+
process.stderr.write(`${HOOK_PREFIX} could not read stdin (${msg}). Allowing.\n`);
|
|
89
|
+
respond("allow");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @type {Record<string, unknown>} */
|
|
94
|
+
let hookPayload;
|
|
95
|
+
try {
|
|
96
|
+
hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
|
|
97
|
+
} catch (e) {
|
|
98
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
99
|
+
process.stderr.write(`${HOOK_PREFIX} invalid JSON (${msg}). Allowing.\n`);
|
|
100
|
+
respond("allow");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const toolName = typeof hookPayload.tool_name === "string" ? hookPayload.tool_name : "";
|
|
105
|
+
|
|
106
|
+
const mapped = mapToolName(toolName);
|
|
107
|
+
if (mapped === null) {
|
|
108
|
+
respond("allow");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const { service, actionType } = mapped;
|
|
112
|
+
|
|
113
|
+
const toolInput =
|
|
114
|
+
typeof hookPayload.tool_input === "object" && hookPayload.tool_input !== null
|
|
115
|
+
? /** @type {Record<string, unknown>} */ (hookPayload.tool_input)
|
|
116
|
+
: {};
|
|
117
|
+
|
|
118
|
+
const config = loadConfig();
|
|
119
|
+
if (config === null || config.apiKey.length === 0 || config.agentName.length === 0) {
|
|
120
|
+
respond("allow");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const paramsSerialized = scrubParameters(toolInput);
|
|
125
|
+
const approvalsUrl = dashboardHintUrl(config.baseUrl);
|
|
126
|
+
|
|
127
|
+
/** @type {Record<string, unknown>} */
|
|
128
|
+
const metadata = {
|
|
129
|
+
tool_name: toolName,
|
|
130
|
+
session_id: typeof hookPayload.session_id === "string" ? hookPayload.session_id : "",
|
|
131
|
+
cwd: typeof hookPayload.cwd === "string" ? hookPayload.cwd : "",
|
|
132
|
+
parameters: paramsSerialized,
|
|
133
|
+
source: "gemini-cli",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** @type {Record<string, unknown>} */
|
|
137
|
+
const payload = {
|
|
138
|
+
agent: config.agentName,
|
|
139
|
+
service,
|
|
140
|
+
actionType,
|
|
141
|
+
status: "pending",
|
|
142
|
+
metadata,
|
|
143
|
+
platform: "gemini-cli",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
let statusCode;
|
|
147
|
+
let bodyText;
|
|
148
|
+
try {
|
|
149
|
+
const res = await postJson(config.baseUrl, config.apiKey, payload);
|
|
150
|
+
statusCode = res.statusCode;
|
|
151
|
+
bodyText = res.bodyText;
|
|
152
|
+
} catch (e) {
|
|
153
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
154
|
+
process.stderr.write(`${HOOK_PREFIX} Shield API unreachable (${msg}). Allowing.\n`);
|
|
155
|
+
respond("allow");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const parsed = safeJsonParse(bodyText);
|
|
160
|
+
const data = unwrapData(parsed);
|
|
161
|
+
|
|
162
|
+
if (statusCode === 202) {
|
|
163
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
164
|
+
openBrowser(url);
|
|
165
|
+
respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (statusCode === 201) {
|
|
170
|
+
if (data === null || typeof data !== "object") {
|
|
171
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
172
|
+
respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const st = String(/** @type {Record<string, unknown>} */ (data).status || "").toLowerCase();
|
|
176
|
+
if (st === "approved") {
|
|
177
|
+
respond("allow");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (st === "blocked") {
|
|
181
|
+
respond("deny", blockedReason(data, approvalsUrl));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
185
|
+
respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
190
|
+
respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main().catch((e) => {
|
|
194
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
195
|
+
process.stderr.write(`${HOOK_PREFIX} unexpected error (${msg}). Allowing.\n`);
|
|
196
|
+
respond("allow");
|
|
197
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Shared helpers for Gemini CLI BeforeTool / AfterTool Shield hooks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const http = require("node:http");
|
|
11
|
+
const https = require("node:https");
|
|
12
|
+
const os = require("node:os");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
|
|
15
|
+
const AUTH_HEADER = "X-Multicorn-Key";
|
|
16
|
+
const HTTP_REQUEST_TIMEOUT_MS = 10000;
|
|
17
|
+
|
|
18
|
+
/** Tools that should pass through without calling Shield (internal / UX-only). */
|
|
19
|
+
const SKIP_TOOLS = new Set([
|
|
20
|
+
"save_memory",
|
|
21
|
+
"activate_skill",
|
|
22
|
+
"get_internal_docs",
|
|
23
|
+
"ask_user",
|
|
24
|
+
"write_todos",
|
|
25
|
+
"enter_plan_mode",
|
|
26
|
+
"exit_plan_mode",
|
|
27
|
+
"update_topic",
|
|
28
|
+
"complete_task",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** @type {Readonly<Record<string, { service: string; actionType: string }>>} */
|
|
32
|
+
const TOOL_MAP = {
|
|
33
|
+
read_file: { service: "filesystem", actionType: "read" },
|
|
34
|
+
read_many_files: { service: "filesystem", actionType: "read" },
|
|
35
|
+
list_directory: { service: "filesystem", actionType: "read" },
|
|
36
|
+
glob: { service: "filesystem", actionType: "read" },
|
|
37
|
+
grep_search: { service: "filesystem", actionType: "read" },
|
|
38
|
+
write_file: { service: "filesystem", actionType: "write" },
|
|
39
|
+
replace: { service: "filesystem", actionType: "write" },
|
|
40
|
+
run_shell_command: { service: "terminal", actionType: "execute" },
|
|
41
|
+
google_web_search: { service: "browser", actionType: "execute" },
|
|
42
|
+
web_fetch: { service: "browser", actionType: "execute" },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function logPrefix(label) {
|
|
46
|
+
return `[multicorn-shield] Gemini CLI ${label}:`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readStdin() {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
process.stdin.setEncoding("utf8");
|
|
53
|
+
process.stdin.on("data", (c) => chunks.push(c));
|
|
54
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
55
|
+
process.stdin.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveGeminiCliAgentName(obj) {
|
|
60
|
+
const agents = obj.agents;
|
|
61
|
+
if (Array.isArray(agents)) {
|
|
62
|
+
for (const entry of agents) {
|
|
63
|
+
if (
|
|
64
|
+
entry &&
|
|
65
|
+
typeof entry === "object" &&
|
|
66
|
+
/** @type {{ platform?: string; name?: string }} */ (entry).platform === "gemini-cli" &&
|
|
67
|
+
typeof (/** @type {{ platform?: string; name?: string }} */ (entry).name) === "string"
|
|
68
|
+
) {
|
|
69
|
+
return /** @type {{ name: string }} */ (entry).name;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return typeof obj.agentName === "string" ? obj.agentName : "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function loadConfig() {
|
|
77
|
+
try {
|
|
78
|
+
const configPath = path.join(os.homedir(), ".multicorn", "config.json");
|
|
79
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
80
|
+
const obj = JSON.parse(raw);
|
|
81
|
+
const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : "";
|
|
82
|
+
const baseUrl =
|
|
83
|
+
typeof obj.baseUrl === "string" && obj.baseUrl.length > 0
|
|
84
|
+
? obj.baseUrl.replace(/\/+$/, "")
|
|
85
|
+
: "https://api.multicorn.ai";
|
|
86
|
+
const baseLower = baseUrl.toLowerCase();
|
|
87
|
+
const isHttps = baseLower.startsWith("https://");
|
|
88
|
+
const isLocal = baseLower.includes("localhost") || baseLower.includes("127.0.0.1");
|
|
89
|
+
if (!isHttps && !isLocal) {
|
|
90
|
+
process.stderr.write(
|
|
91
|
+
`${logPrefix("config")} baseUrl must use HTTPS for non-local servers. Fail-open: Shield disabled.\n`,
|
|
92
|
+
);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const agentName = resolveGeminiCliAgentName(obj);
|
|
96
|
+
return { apiKey, baseUrl, agentName };
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} toolName
|
|
104
|
+
* @returns {{ service: string; actionType: string } | null} null = skip hook API calls
|
|
105
|
+
*/
|
|
106
|
+
function mapToolName(toolName) {
|
|
107
|
+
const name = String(toolName || "").trim();
|
|
108
|
+
if (name.length === 0) return null;
|
|
109
|
+
if (SKIP_TOOLS.has(name)) return null;
|
|
110
|
+
|
|
111
|
+
if (name.startsWith("mcp_")) {
|
|
112
|
+
const rest = name.slice(4);
|
|
113
|
+
const idx = rest.indexOf("_");
|
|
114
|
+
if (idx <= 0) {
|
|
115
|
+
const safe = rest.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
116
|
+
return { service: `mcp:${safe}`, actionType: "execute" };
|
|
117
|
+
}
|
|
118
|
+
const server = rest.slice(0, idx).replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
119
|
+
const tool = rest.slice(idx + 1).replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
120
|
+
return { service: `mcp:${server}.${tool}`, actionType: "execute" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mapped = TOOL_MAP[name];
|
|
124
|
+
if (mapped !== undefined) {
|
|
125
|
+
return mapped;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { service: "unknown", actionType: "execute" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function postJson(baseUrl, apiKey, bodyObj) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
let u;
|
|
134
|
+
try {
|
|
135
|
+
const root = String(baseUrl).replace(/\/+$/, "");
|
|
136
|
+
u = new URL(`${root}/api/v1/actions`);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
reject(e);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const payload = JSON.stringify(bodyObj);
|
|
142
|
+
const isHttps = u.protocol === "https:";
|
|
143
|
+
const lib = isHttps ? https : http;
|
|
144
|
+
const port = u.port || (isHttps ? 443 : 80);
|
|
145
|
+
const hostname = u.hostname;
|
|
146
|
+
/** @type {string} */
|
|
147
|
+
const pathnamePlusSearch = u.pathname + u.search;
|
|
148
|
+
const options = {
|
|
149
|
+
hostname,
|
|
150
|
+
port,
|
|
151
|
+
path: pathnamePlusSearch,
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
Connection: "close",
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
"Content-Length": Buffer.byteLength(payload, "utf8"),
|
|
157
|
+
[AUTH_HEADER]: apiKey,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
const req = lib.request(options, (res) => {
|
|
161
|
+
const chunks = [];
|
|
162
|
+
res.on("data", (c) => chunks.push(c));
|
|
163
|
+
res.on("end", () => {
|
|
164
|
+
resolve({
|
|
165
|
+
statusCode: res.statusCode ?? 0,
|
|
166
|
+
bodyText: Buffer.concat(chunks).toString("utf8"),
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
req.setTimeout(HTTP_REQUEST_TIMEOUT_MS, () => {
|
|
171
|
+
req.destroy(new Error("request timeout"));
|
|
172
|
+
});
|
|
173
|
+
req.on("error", reject);
|
|
174
|
+
req.write(payload);
|
|
175
|
+
req.end();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function safeJsonParse(text) {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(text);
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function unwrapData(body) {
|
|
188
|
+
if (typeof body !== "object" || body === null) return null;
|
|
189
|
+
const o = /** @type {Record<string, unknown>} */ (body);
|
|
190
|
+
return o.success === true ? o.data : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function applyParameterScrub(parameters) {
|
|
194
|
+
const scrubbedParams = { ...parameters };
|
|
195
|
+
if (typeof scrubbedParams.content === "string") {
|
|
196
|
+
scrubbedParams.content = `[${scrubbedParams.content.length} chars redacted]`;
|
|
197
|
+
}
|
|
198
|
+
if (typeof scrubbedParams.command === "string" && scrubbedParams.command.length > 200) {
|
|
199
|
+
scrubbedParams.command = scrubbedParams.command.slice(0, 200) + "... [truncated]";
|
|
200
|
+
}
|
|
201
|
+
return scrubbedParams;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function scrubParameters(parameters, maxLen = 4096) {
|
|
205
|
+
/** @type {Record<string, unknown>} */
|
|
206
|
+
let base = {};
|
|
207
|
+
if (typeof parameters === "object" && parameters !== null) {
|
|
208
|
+
base = { .../** @type {Record<string, unknown>} */ (parameters) };
|
|
209
|
+
} else if (typeof parameters === "string") {
|
|
210
|
+
try {
|
|
211
|
+
const p = JSON.parse(parameters);
|
|
212
|
+
if (p !== null && typeof p === "object") {
|
|
213
|
+
base = { .../** @type {Record<string, unknown>} */ (p) };
|
|
214
|
+
} else {
|
|
215
|
+
base = { raw: parameters };
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
base = { raw: parameters };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const scrubbed = applyParameterScrub(base);
|
|
223
|
+
let paramsSerialized;
|
|
224
|
+
try {
|
|
225
|
+
paramsSerialized = JSON.stringify(scrubbed);
|
|
226
|
+
} catch {
|
|
227
|
+
paramsSerialized = "{}";
|
|
228
|
+
}
|
|
229
|
+
if (paramsSerialized.length > maxLen) {
|
|
230
|
+
paramsSerialized = paramsSerialized.slice(0, maxLen);
|
|
231
|
+
}
|
|
232
|
+
return paramsSerialized;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function scrubResultForMetadata(result) {
|
|
236
|
+
if (result === null || result === undefined) return "";
|
|
237
|
+
let s;
|
|
238
|
+
try {
|
|
239
|
+
s = typeof result === "string" ? result : JSON.stringify(result);
|
|
240
|
+
} catch {
|
|
241
|
+
s = String(result);
|
|
242
|
+
}
|
|
243
|
+
s = s.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]");
|
|
244
|
+
s = s.replace(/\bmcs_[A-Za-z0-9_-]+\b/g, "[REDACTED]");
|
|
245
|
+
s = s.replace(/\bghp_[A-Za-z0-9]{20,}\b/g, "[REDACTED]");
|
|
246
|
+
s = s.replace(/Bearer\s+[^\s]+/gi, "[REDACTED]");
|
|
247
|
+
s = s.replace(/\b(password|token)\s*[:=]\s*[^\s]+\b/gi, "[REDACTED]");
|
|
248
|
+
if (s.length > 500) {
|
|
249
|
+
s = s.slice(0, 500) + "[truncated]";
|
|
250
|
+
}
|
|
251
|
+
return s;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} apiBaseUrl
|
|
256
|
+
* @param {string} agentName
|
|
257
|
+
* @param {string} service
|
|
258
|
+
* @param {string} actionType
|
|
259
|
+
*/
|
|
260
|
+
function consentUrl(apiBaseUrl, agentName, service, actionType) {
|
|
261
|
+
let origin = "https://app.multicorn.ai";
|
|
262
|
+
try {
|
|
263
|
+
const raw = String(apiBaseUrl).replace(/\/+$/, "");
|
|
264
|
+
const lower = raw.toLowerCase();
|
|
265
|
+
if (lower.includes("localhost:8080") || lower.includes("127.0.0.1:8080")) {
|
|
266
|
+
origin = "http://localhost:5173";
|
|
267
|
+
} else {
|
|
268
|
+
const u = new URL(raw);
|
|
269
|
+
if (u.hostname.startsWith("api.")) {
|
|
270
|
+
u.hostname = "app." + u.hostname.slice(4);
|
|
271
|
+
}
|
|
272
|
+
origin = u.origin;
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
/* keep default */
|
|
276
|
+
}
|
|
277
|
+
const params = new URLSearchParams();
|
|
278
|
+
params.set("agent", agentName);
|
|
279
|
+
params.set("scopes", `${service}:${actionType}`);
|
|
280
|
+
params.set("platform", "gemini-cli");
|
|
281
|
+
return `${origin}/consent?${params.toString()}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @param {string} url */
|
|
285
|
+
function openBrowser(url) {
|
|
286
|
+
try {
|
|
287
|
+
const { execFileSync } = require("node:child_process");
|
|
288
|
+
if (process.platform === "darwin") {
|
|
289
|
+
execFileSync("open", [url], { stdio: "ignore" });
|
|
290
|
+
} else if (process.platform === "win32") {
|
|
291
|
+
execFileSync("cmd.exe", ["/c", "start", "", url], {
|
|
292
|
+
stdio: "ignore",
|
|
293
|
+
windowsHide: true,
|
|
294
|
+
});
|
|
295
|
+
} else {
|
|
296
|
+
execFileSync("xdg-open", [url], { stdio: "ignore" });
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
/* ignore */
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
AUTH_HEADER,
|
|
305
|
+
logPrefix,
|
|
306
|
+
HTTP_REQUEST_TIMEOUT_MS,
|
|
307
|
+
TOOL_MAP,
|
|
308
|
+
readStdin,
|
|
309
|
+
loadConfig,
|
|
310
|
+
resolveGeminiCliAgentName,
|
|
311
|
+
mapToolName,
|
|
312
|
+
postJson,
|
|
313
|
+
safeJsonParse,
|
|
314
|
+
unwrapData,
|
|
315
|
+
scrubParameters,
|
|
316
|
+
scrubResultForMetadata,
|
|
317
|
+
consentUrl,
|
|
318
|
+
openBrowser,
|
|
319
|
+
};
|