@myvillage/cli 2.14.0 → 2.15.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/package.json
CHANGED
|
@@ -167,7 +167,10 @@ export async function getMCPTools(agentDir, agentConfig) {
|
|
|
167
167
|
// withhold everything else this server offers (e.g. grant `network_feed`
|
|
168
168
|
// but not `wallet_send`). Absent/empty → all tools, the backward-compatible
|
|
169
169
|
// default.
|
|
170
|
-
const tools =
|
|
170
|
+
const tools = withSecretPlaceholders(
|
|
171
|
+
applyToolAllowlist(await client.tools(), server.allowed_tools, name),
|
|
172
|
+
server.secrets,
|
|
173
|
+
);
|
|
171
174
|
const toolNames = Object.keys(tools);
|
|
172
175
|
Object.assign(allTools, tools);
|
|
173
176
|
servers.push({
|
|
@@ -216,6 +219,76 @@ export function applyToolAllowlist(tools, allowed, serverName) {
|
|
|
216
219
|
return filtered;
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Let an agent use a secret without ever seeing it.
|
|
224
|
+
*
|
|
225
|
+
* `secrets` is a server's list of `.env` names from tools.yaml (e.g.
|
|
226
|
+
* `[SCHOOL_USERNAME, SCHOOL_PASSWORD]`). The agent writes `${SCHOOL_PASSWORD}`
|
|
227
|
+
* in a tool call, say as the value for `puppeteer_fill`, and the real value is
|
|
228
|
+
* swapped in right before the tool runs. Anything the tool sends back has the
|
|
229
|
+
* value turned back into its placeholder, so the secret never reaches the
|
|
230
|
+
* model, its context, or the activity log.
|
|
231
|
+
*
|
|
232
|
+
* Only the names listed are swapped, per server, so an agent cannot ask for
|
|
233
|
+
* `${MYVILLAGE_ACCESS_TOKEN}` or any other variable it wasn't given. Unknown or
|
|
234
|
+
* unset names are left as written.
|
|
235
|
+
*/
|
|
236
|
+
export function withSecretPlaceholders(tools, secretNames, env = process.env) {
|
|
237
|
+
if (!Array.isArray(secretNames) || secretNames.length === 0) return tools;
|
|
238
|
+
const allowed = new Set(secretNames.filter((n) => typeof n === 'string' && /^\w+$/.test(n)));
|
|
239
|
+
if (allowed.size === 0) return tools;
|
|
240
|
+
|
|
241
|
+
const fill = (v) => {
|
|
242
|
+
if (typeof v === 'string') {
|
|
243
|
+
return v.replace(/\$\{(\w+)\}/g, (m, n) => (allowed.has(n) && env[n] ? env[n] : m));
|
|
244
|
+
}
|
|
245
|
+
if (Array.isArray(v)) return v.map(fill);
|
|
246
|
+
if (v && typeof v === 'object') {
|
|
247
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, fill(x)]));
|
|
248
|
+
}
|
|
249
|
+
return v;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Longest first, so a value that contains another is replaced whole. Very
|
|
253
|
+
// short values are skipped: blanking every "a" in a page would wreck it.
|
|
254
|
+
const values = [...allowed]
|
|
255
|
+
.map((n) => [n, env[n]])
|
|
256
|
+
.filter(([, v]) => typeof v === 'string' && v.length >= 3)
|
|
257
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
258
|
+
const redact = (v) => {
|
|
259
|
+
if (typeof v === 'string') {
|
|
260
|
+
let out = v;
|
|
261
|
+
for (const [n, secret] of values) out = out.split(secret).join(`\${${n}}`);
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
if (Array.isArray(v)) return v.map(redact);
|
|
265
|
+
if (v && typeof v === 'object') {
|
|
266
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, redact(x)]));
|
|
267
|
+
}
|
|
268
|
+
return v;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const wrapped = {};
|
|
272
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
273
|
+
if (typeof tool?.execute !== 'function') {
|
|
274
|
+
wrapped[name] = tool;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
wrapped[name] = {
|
|
278
|
+
...tool,
|
|
279
|
+
execute: async (args, options) => {
|
|
280
|
+
try {
|
|
281
|
+
return redact(await tool.execute(fill(args), options));
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (err && typeof err.message === 'string') err.message = redact(err.message);
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return wrapped;
|
|
290
|
+
}
|
|
291
|
+
|
|
219
292
|
export async function cleanupMCPClients() {
|
|
220
293
|
for (const client of activeClients) {
|
|
221
294
|
try { await client.close(); } catch { /* ignore */ }
|