@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 = applyToolAllowlist(await client.tools(), server.allowed_tools, name);
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 */ }