@myvillage/cli 1.74.0 → 1.76.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
|
@@ -16,9 +16,39 @@ import { tool } from 'ai';
|
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { appendRows, datasetExists, readManifest, ingestAsset } from '../utils/dataset-workspace.js';
|
|
18
18
|
import { DATASET_APPEND_DESCRIPTION, MODEL_PREDICT_DESCRIPTION } from './builtin-tools-registry.js';
|
|
19
|
+
import { readToolsYaml } from '../utils/local-agent.js';
|
|
19
20
|
|
|
20
21
|
export { BUILTIN_TOOL_DEFS } from './builtin-tools-registry.js';
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Gate built-ins by the SAME allowlist as the MyVillageOS platform server
|
|
25
|
+
* (`allowed_tools` on whichever tools.yaml server points at a myvillageproject.ai
|
|
26
|
+
* URL). This lets built-ins be scoped from one place — the myvillage tool card —
|
|
27
|
+
* rather than a separate mechanism. Semantics match `applyToolAllowlist`:
|
|
28
|
+
* - no allowlist on that server → all built-ins load (back-compatible)
|
|
29
|
+
* - allowlist present → a built-in loads only if explicitly listed
|
|
30
|
+
*/
|
|
31
|
+
function applyBuiltinAllowlist(builtins, agentName) {
|
|
32
|
+
let allowed;
|
|
33
|
+
try {
|
|
34
|
+
const servers = readToolsYaml(agentName)?.servers ?? {};
|
|
35
|
+
const platform = Object.values(servers).find(
|
|
36
|
+
(s) =>
|
|
37
|
+
s && typeof s === 'object' && typeof s.url === 'string' &&
|
|
38
|
+
/(^|\.)myvillageproject\.ai/i.test(new URL(s.url).hostname),
|
|
39
|
+
);
|
|
40
|
+
allowed = platform?.allowed_tools;
|
|
41
|
+
} catch {
|
|
42
|
+
allowed = undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(allowed)) return builtins;
|
|
45
|
+
const filtered = {};
|
|
46
|
+
for (const [name, t] of Object.entries(builtins)) {
|
|
47
|
+
if (allowed.includes(name)) filtered[name] = t;
|
|
48
|
+
}
|
|
49
|
+
return filtered;
|
|
50
|
+
}
|
|
51
|
+
|
|
22
52
|
/**
|
|
23
53
|
* Build the native built-in tools for an agent.
|
|
24
54
|
* @param {string} agentName
|
|
@@ -26,7 +56,7 @@ export { BUILTIN_TOOL_DEFS } from './builtin-tools-registry.js';
|
|
|
26
56
|
* @returns {Record<string, object>} AI SDK tools keyed by tool name
|
|
27
57
|
*/
|
|
28
58
|
export function getBuiltinTools(agentName, config) {
|
|
29
|
-
|
|
59
|
+
const all = {
|
|
30
60
|
dataset_append: tool({
|
|
31
61
|
description: DATASET_APPEND_DESCRIPTION,
|
|
32
62
|
inputSchema: z.object({
|
|
@@ -122,4 +152,5 @@ export function getBuiltinTools(agentName, config) {
|
|
|
122
152
|
},
|
|
123
153
|
}),
|
|
124
154
|
};
|
|
155
|
+
return applyBuiltinAllowlist(all, agentName);
|
|
125
156
|
}
|
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
import { readFileSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { parse as parseYaml } from 'yaml';
|
|
8
|
+
import { BUILTIN_TOOL_DEFS } from './builtin-tools-registry.js';
|
|
8
9
|
|
|
9
10
|
const activeClients = [];
|
|
10
11
|
|
|
12
|
+
// Built-in tools share the MyVillageOS server's `allowed_tools` list (they're
|
|
13
|
+
// scoped from the same card), so their names legitimately appear there even
|
|
14
|
+
// though they aren't exposed by any MCP server — don't flag them as "unknown".
|
|
15
|
+
const BUILTIN_TOOL_NAMES = new Set(BUILTIN_TOOL_DEFS.map((d) => d.name));
|
|
16
|
+
|
|
11
17
|
// ── Public API ──────────────────────────────────────────
|
|
12
18
|
|
|
13
19
|
export async function getMCPTools(agentDir, agentConfig) {
|
|
@@ -201,7 +207,7 @@ export function applyToolAllowlist(tools, allowed, serverName) {
|
|
|
201
207
|
for (const [name, tool] of Object.entries(tools)) {
|
|
202
208
|
if (allow.has(name)) filtered[name] = tool;
|
|
203
209
|
}
|
|
204
|
-
const unknown = allowed.filter((n) => !(n in tools));
|
|
210
|
+
const unknown = allowed.filter((n) => !(n in tools) && !BUILTIN_TOOL_NAMES.has(n));
|
|
205
211
|
if (unknown.length > 0) {
|
|
206
212
|
console.error(
|
|
207
213
|
`[mcp-client] ${serverName}: allowed_tools lists ${unknown.length} tool(s) this server doesn't expose: ${unknown.join(', ')}`,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
3
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
4
|
+
import { agentExists, readAgentConfig } from '../utils/local-agent.js';
|
|
5
|
+
import {
|
|
6
|
+
listAgentCredentialGrants,
|
|
7
|
+
grantAgentCredential,
|
|
8
|
+
revokeAgentCredential,
|
|
9
|
+
} from '../utils/api.js';
|
|
10
|
+
|
|
11
|
+
// ── Agent OAuth credential grants ───────────────────────
|
|
12
|
+
//
|
|
13
|
+
// `myvillage agent grant/revoke/grants <name> [provider]` — authorize a local
|
|
14
|
+
// agent to use the villager's connected providers (Google, Microsoft, Zoom)
|
|
15
|
+
// when it runs through the MyVillage MCP server. The platform vends
|
|
16
|
+
// short-lived tokens to the MCP server; raw OAuth tokens never touch the CLI.
|
|
17
|
+
//
|
|
18
|
+
// The villager connects the provider once in MyVillage Studio or at
|
|
19
|
+
// myvillageproject.ai. These commands only manage the per-agent consent.
|
|
20
|
+
|
|
21
|
+
const VALID_PROVIDERS = ['google', 'microsoft', 'zoom'];
|
|
22
|
+
|
|
23
|
+
// Human-facing labels keyed by the lowercase slug the CLI accepts.
|
|
24
|
+
const PROVIDER_LABELS = {
|
|
25
|
+
google: 'Google (Gmail, Calendar, Drive)',
|
|
26
|
+
microsoft: 'Microsoft 365 (Outlook mail, Calendar)',
|
|
27
|
+
zoom: 'Zoom',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function fail(message) {
|
|
31
|
+
console.log(chalk.red(` ✗ ${message}`));
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeProvider(provider) {
|
|
36
|
+
return (provider || '').trim().toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Resolve the AgentProfile id (man.agent_id) for a local agent, with friendly
|
|
40
|
+
// errors when the agent is unknown or hasn't been registered yet.
|
|
41
|
+
function resolveAgentId(name) {
|
|
42
|
+
if (!agentExists(name)) {
|
|
43
|
+
return { error: `Agent "${name}" not found. Run \`myvillage agent\` to see your agents.` };
|
|
44
|
+
}
|
|
45
|
+
const config = readAgentConfig(name);
|
|
46
|
+
const agentId = config?.man?.agent_id;
|
|
47
|
+
if (!agentId) {
|
|
48
|
+
return {
|
|
49
|
+
error:
|
|
50
|
+
`Agent "${name}" hasn't been registered with the network yet. ` +
|
|
51
|
+
`Run \`myvillage agent start ${name}\` once so it gets an agent id, then retry.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { agentId };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Map a platform error response to a villager-readable, actionable message.
|
|
58
|
+
function describeGrantError(err, provider) {
|
|
59
|
+
const data = err.response?.data;
|
|
60
|
+
const code = data?.code;
|
|
61
|
+
const label = PROVIDER_LABELS[provider] || provider;
|
|
62
|
+
switch (code) {
|
|
63
|
+
case 'CREDENTIAL_NOT_CONNECTED':
|
|
64
|
+
return `${label} isn't connected for your account. Connect it in MyVillage Studio or at myvillageproject.ai, then retry.`;
|
|
65
|
+
case 'GRANT_NOT_FOUND':
|
|
66
|
+
return `No active ${label} grant for this agent.`;
|
|
67
|
+
case 'FORBIDDEN_NOT_OWNER':
|
|
68
|
+
return `You don't own this agent, so you can't change its credential grants.`;
|
|
69
|
+
case 'AGENT_NOT_FOUND':
|
|
70
|
+
return `The platform has no record of this agent. Run \`myvillage agent start\` to register it.`;
|
|
71
|
+
default:
|
|
72
|
+
return data?.error || err.message;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── grant ───────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export async function agentGrantCommand(name, provider) {
|
|
79
|
+
if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
|
|
80
|
+
|
|
81
|
+
const slug = normalizeProvider(provider);
|
|
82
|
+
if (!VALID_PROVIDERS.includes(slug)) {
|
|
83
|
+
return fail(`Invalid provider "${provider}". Must be one of: ${VALID_PROVIDERS.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { agentId, error } = resolveAgentId(name);
|
|
87
|
+
if (error) return fail(error);
|
|
88
|
+
|
|
89
|
+
const label = PROVIDER_LABELS[slug];
|
|
90
|
+
const spinner = villageSpinner(`Granting ${label} to "${name}"...`).start();
|
|
91
|
+
try {
|
|
92
|
+
await grantAgentCredential(agentId, slug.toUpperCase());
|
|
93
|
+
spinner.succeed(`"${name}" can now use your ${label}.`);
|
|
94
|
+
console.log(brand.teal(`\n The agent reads credentials through the MyVillage MCP server — no tokens are stored locally.`));
|
|
95
|
+
console.log(brand.teal(` Revoke any time: ${chalk.bold(`myvillage agent revoke ${name} ${slug}`)}\n`));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
spinner.fail(`Failed to grant ${label}: ${describeGrantError(err, slug)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── revoke ──────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export async function agentRevokeCredentialCommand(name, provider) {
|
|
104
|
+
if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
|
|
105
|
+
|
|
106
|
+
const slug = normalizeProvider(provider);
|
|
107
|
+
if (!VALID_PROVIDERS.includes(slug)) {
|
|
108
|
+
return fail(`Invalid provider "${provider}". Must be one of: ${VALID_PROVIDERS.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { agentId, error } = resolveAgentId(name);
|
|
112
|
+
if (error) return fail(error);
|
|
113
|
+
|
|
114
|
+
const label = PROVIDER_LABELS[slug];
|
|
115
|
+
const spinner = villageSpinner(`Revoking ${label} from "${name}"...`).start();
|
|
116
|
+
try {
|
|
117
|
+
await revokeAgentCredential(agentId, slug.toUpperCase());
|
|
118
|
+
spinner.succeed(`Revoked ${label} from "${name}". (Propagates to running agents within ~60s.)`);
|
|
119
|
+
console.log('');
|
|
120
|
+
} catch (err) {
|
|
121
|
+
spinner.fail(`Failed to revoke ${label}: ${describeGrantError(err, slug)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── grants (list) ───────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export async function agentGrantsListCommand(name) {
|
|
128
|
+
if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
|
|
129
|
+
|
|
130
|
+
const { agentId, error } = resolveAgentId(name);
|
|
131
|
+
if (error) return fail(error);
|
|
132
|
+
|
|
133
|
+
const spinner = villageSpinner(`Loading credential grants for "${name}"...`).start();
|
|
134
|
+
try {
|
|
135
|
+
const result = await listAgentCredentialGrants(agentId);
|
|
136
|
+
const grants = result?.data || result || [];
|
|
137
|
+
spinner.stop();
|
|
138
|
+
|
|
139
|
+
if (!Array.isArray(grants) || grants.length === 0) {
|
|
140
|
+
console.log(brand.teal(`\n "${name}" has no active credential grants.`));
|
|
141
|
+
console.log(brand.teal(` Authorize one: ${chalk.bold(`myvillage agent grant ${name} <google|microsoft|zoom>`)}\n`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(chalk.bold(` Credential grants for "${name}"`));
|
|
147
|
+
console.log('');
|
|
148
|
+
for (const g of grants) {
|
|
149
|
+
const slug = String(g.provider || '').toLowerCase();
|
|
150
|
+
const label = PROVIDER_LABELS[slug] || g.provider;
|
|
151
|
+
const when = g.grantedAt ? ` ${chalk.dim(`(granted ${new Date(g.grantedAt).toLocaleDateString()})`)}` : '';
|
|
152
|
+
console.log(` ${brand.green('●')} ${label}${when}`);
|
|
153
|
+
}
|
|
154
|
+
console.log('');
|
|
155
|
+
} catch (err) {
|
|
156
|
+
spinner.fail(`Failed to load grants: ${err.response?.data?.error || err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/index.js
CHANGED
|
@@ -84,6 +84,11 @@ import {
|
|
|
84
84
|
agentListMcpCommand,
|
|
85
85
|
recipesListCommand,
|
|
86
86
|
} from './commands/agent-mcp.js';
|
|
87
|
+
import {
|
|
88
|
+
agentGrantCommand,
|
|
89
|
+
agentRevokeCredentialCommand,
|
|
90
|
+
agentGrantsListCommand,
|
|
91
|
+
} from './commands/agent-credentials.js';
|
|
87
92
|
import {
|
|
88
93
|
gameUpdateCommand,
|
|
89
94
|
gameUploadThumbnailCommand,
|
|
@@ -553,6 +558,24 @@ export function run() {
|
|
|
553
558
|
.description('List the MCP servers currently configured for an agent')
|
|
554
559
|
.action(agentListMcpCommand);
|
|
555
560
|
|
|
561
|
+
// Per-agent OAuth credential grants (Google/Microsoft/Zoom). The villager
|
|
562
|
+
// connects the provider once in the app/portal; these authorize a specific
|
|
563
|
+
// agent to use it via the MyVillage MCP server. Tokens never touch the CLI.
|
|
564
|
+
agentCmd
|
|
565
|
+
.command('grant <name> <provider>')
|
|
566
|
+
.description('Authorize an agent to use a connected provider (google, microsoft, zoom)')
|
|
567
|
+
.action(agentGrantCommand);
|
|
568
|
+
|
|
569
|
+
agentCmd
|
|
570
|
+
.command('revoke <name> <provider>')
|
|
571
|
+
.description('Revoke an agent\'s access to a provider (google, microsoft, zoom)')
|
|
572
|
+
.action(agentRevokeCredentialCommand);
|
|
573
|
+
|
|
574
|
+
agentCmd
|
|
575
|
+
.command('grants <name>')
|
|
576
|
+
.description('List an agent\'s active OAuth credential grants')
|
|
577
|
+
.action(agentGrantsListCommand);
|
|
578
|
+
|
|
556
579
|
program
|
|
557
580
|
.command('recipes')
|
|
558
581
|
.description('List the MCP recipes bundled with the CLI')
|
package/src/utils/api.js
CHANGED
|
@@ -777,3 +777,31 @@ export async function shareKnowledgeAsAgent(data) {
|
|
|
777
777
|
const response = await client.post('/agent-tools/share-knowledge', data);
|
|
778
778
|
return response.data;
|
|
779
779
|
}
|
|
780
|
+
|
|
781
|
+
// ── Agent OAuth credential grants ───────────────────────
|
|
782
|
+
//
|
|
783
|
+
// Per-agent authorization to use the villager's connected providers
|
|
784
|
+
// (GOOGLE / MICROSOFT / ZOOM) when the agent runs through the MyVillage MCP
|
|
785
|
+
// server. The platform vends short-lived tokens to the MCP server — the raw
|
|
786
|
+
// OAuth tokens never reach the CLI. `agentId` is the AgentProfile id, i.e.
|
|
787
|
+
// the local agent's `man.agent_id` (populated on first `agent start`).
|
|
788
|
+
//
|
|
789
|
+
// Endpoints live under platformBaseUrl (/api), not /api/v1.
|
|
790
|
+
|
|
791
|
+
export async function listAgentCredentialGrants(agentId) {
|
|
792
|
+
const client = getPlatformClient();
|
|
793
|
+
const response = await client.get(`/agents/${agentId}/credential-grants`);
|
|
794
|
+
return response.data;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export async function grantAgentCredential(agentId, provider) {
|
|
798
|
+
const client = getPlatformClient();
|
|
799
|
+
const response = await client.post(`/agents/${agentId}/credential-grants`, { provider });
|
|
800
|
+
return response.data;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export async function revokeAgentCredential(agentId, provider) {
|
|
804
|
+
const client = getPlatformClient();
|
|
805
|
+
const response = await client.delete(`/agents/${agentId}/credential-grants/${provider}`);
|
|
806
|
+
return response.data;
|
|
807
|
+
}
|