@vellumai/assistant 0.4.18 → 0.4.19
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/docs/runbook-trusted-contacts.md +5 -3
- package/package.json +1 -1
- package/src/__tests__/channel-approvals.test.ts +7 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/daemon-server-session-init.test.ts +2 -0
- package/src/__tests__/gmail-integration.test.ts +13 -4
- package/src/__tests__/handle-user-message-secret-resume.test.ts +7 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -0
- package/src/__tests__/ingress-reconcile.test.ts +13 -5
- package/src/__tests__/mcp-cli.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +9 -1
- package/src/__tests__/send-endpoint-busy.test.ts +8 -2
- package/src/__tests__/sms-messaging-provider.test.ts +4 -0
- package/src/__tests__/system-prompt.test.ts +21 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/cli/mcp.ts +81 -28
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +6 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -2
- package/src/config/bundled-skills/sms-setup/SKILL.md +8 -16
- package/src/config/bundled-skills/telegram-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/trusted-contacts/SKILL.md +13 -25
- package/src/config/bundled-skills/twilio-setup/SKILL.md +13 -23
- package/src/config/system-prompt.ts +32 -0
- package/src/mcp/client.ts +2 -7
- package/src/security/secure-keys.ts +18 -2
- package/src/tools/terminal/safe-env.ts +7 -0
|
@@ -5,11 +5,13 @@ Operational procedures for inspecting, managing, and debugging the trusted conta
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# Read the bearer token
|
|
9
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
10
|
-
|
|
11
8
|
# Base URL (adjust if using a non-default port)
|
|
12
9
|
BASE=http://localhost:7830
|
|
10
|
+
|
|
11
|
+
# Bearer token: if running via the assistant's shell tools, $GATEWAY_AUTH_TOKEN
|
|
12
|
+
# is injected automatically. For manual operator use, mint a token via the CLI
|
|
13
|
+
# or use one from the daemon (e.g. from a recent shell env export).
|
|
14
|
+
TOKEN=$GATEWAY_AUTH_TOKEN
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
## 1. Inspect Trusted Contacts (Members)
|
package/package.json
CHANGED
|
@@ -132,9 +132,11 @@ describe("getChannelApprovalPrompt", () => {
|
|
|
132
132
|
const result = getChannelApprovalPrompt("conv-1");
|
|
133
133
|
expect(result).not.toBeNull();
|
|
134
134
|
expect(result!.promptText).toContain("shell");
|
|
135
|
-
expect(result!.actions).toHaveLength(
|
|
135
|
+
expect(result!.actions).toHaveLength(5);
|
|
136
136
|
expect(result!.actions.map((a) => a.id)).toEqual([
|
|
137
137
|
"approve_once",
|
|
138
|
+
"approve_10m",
|
|
139
|
+
"approve_thread",
|
|
138
140
|
"approve_always",
|
|
139
141
|
"reject",
|
|
140
142
|
]);
|
|
@@ -174,6 +176,8 @@ describe("getChannelApprovalPrompt", () => {
|
|
|
174
176
|
expect(result).not.toBeNull();
|
|
175
177
|
expect(result!.actions.map((a) => a.id)).toEqual([
|
|
176
178
|
"approve_once",
|
|
179
|
+
"approve_10m",
|
|
180
|
+
"approve_thread",
|
|
177
181
|
"approve_always",
|
|
178
182
|
"reject",
|
|
179
183
|
]);
|
|
@@ -189,6 +193,8 @@ describe("getChannelApprovalPrompt", () => {
|
|
|
189
193
|
expect(result).not.toBeNull();
|
|
190
194
|
expect(result!.actions.map((a) => a.id)).toEqual([
|
|
191
195
|
"approve_once",
|
|
196
|
+
"approve_10m",
|
|
197
|
+
"approve_thread",
|
|
192
198
|
"approve_always",
|
|
193
199
|
"reject",
|
|
194
200
|
]);
|
|
@@ -79,6 +79,14 @@ mock.module("../runtime/local-actor-identity.js", () => ({
|
|
|
79
79
|
}),
|
|
80
80
|
}));
|
|
81
81
|
|
|
82
|
+
mock.module("../runtime/guardian-context-resolver.js", () => ({
|
|
83
|
+
resolveGuardianContext: () => ({
|
|
84
|
+
trustClass: "guardian",
|
|
85
|
+
sourceChannel: "vellum",
|
|
86
|
+
}),
|
|
87
|
+
toGuardianRuntimeContext: (ctx: unknown) => ctx,
|
|
88
|
+
}));
|
|
89
|
+
|
|
82
90
|
import type { AuthContext } from "../runtime/auth/types.js";
|
|
83
91
|
import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
|
|
84
92
|
|
|
@@ -11,6 +11,13 @@ const toolsManifestPath = resolve(
|
|
|
11
11
|
"../config/bundled-skills/messaging/TOOLS.json",
|
|
12
12
|
);
|
|
13
13
|
const toolsManifest = JSON.parse(readFileSync(toolsManifestPath, "utf-8"));
|
|
14
|
+
const slackToolsManifestPath = resolve(
|
|
15
|
+
__dirname,
|
|
16
|
+
"../config/bundled-skills/slack/TOOLS.json",
|
|
17
|
+
);
|
|
18
|
+
const slackToolsManifest = JSON.parse(
|
|
19
|
+
readFileSync(slackToolsManifestPath, "utf-8"),
|
|
20
|
+
);
|
|
14
21
|
|
|
15
22
|
describe("Messaging tool contract", () => {
|
|
16
23
|
const expectedGmailToolNames = [
|
|
@@ -70,19 +77,21 @@ describe("Messaging tool contract", () => {
|
|
|
70
77
|
});
|
|
71
78
|
|
|
72
79
|
test("TOOLS.json manifest contains all expected slack_* tool names", () => {
|
|
73
|
-
const
|
|
80
|
+
const slackToolNames: string[] = slackToolsManifest.tools.map(
|
|
74
81
|
(t: { name: string }) => t.name,
|
|
75
82
|
);
|
|
76
83
|
for (const name of expectedSlackToolNames) {
|
|
77
|
-
expect(
|
|
84
|
+
expect(slackToolNames).toContain(name);
|
|
78
85
|
}
|
|
79
86
|
});
|
|
80
87
|
|
|
81
|
-
test("TOOLS.json
|
|
88
|
+
test("TOOLS.json manifests contain at least the expected number of tools", () => {
|
|
82
89
|
const expectedMinimum =
|
|
83
90
|
expectedGmailToolNames.length +
|
|
84
91
|
expectedMessagingToolNames.length +
|
|
85
92
|
expectedSlackToolNames.length;
|
|
86
|
-
|
|
93
|
+
const totalTools =
|
|
94
|
+
toolsManifest.tools.length + slackToolsManifest.tools.length;
|
|
95
|
+
expect(totalTools).toBeGreaterThanOrEqual(expectedMinimum);
|
|
87
96
|
});
|
|
88
97
|
});
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
2
|
import { describe, expect, mock, test } from "bun:test";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const actualEnv = await import("../config/env.js");
|
|
5
|
+
mock.module("../config/env.js", () => ({
|
|
6
|
+
...actualEnv,
|
|
7
|
+
isHttpAuthDisabled: () => true,
|
|
8
|
+
isMonitoringEnabled: () => false,
|
|
9
|
+
}));
|
|
5
10
|
|
|
6
11
|
const { handleUserMessage } = await import("../daemon/handlers/sessions.js");
|
|
7
12
|
|
|
@@ -47,6 +52,7 @@ describe("handleUserMessage secret redirect continuation", () => {
|
|
|
47
52
|
setAssistantId: () => {},
|
|
48
53
|
setChannelCapabilities: () => {},
|
|
49
54
|
setGuardianContext: () => {},
|
|
55
|
+
setAuthContext: () => {},
|
|
50
56
|
setCommandIntent: () => {},
|
|
51
57
|
updateClient: () => {},
|
|
52
58
|
emitActivityState: () => {},
|
|
@@ -116,6 +116,7 @@ interface TestSession {
|
|
|
116
116
|
setTurnInterfaceContext: (ctx: unknown) => void;
|
|
117
117
|
setAssistantId: (assistantId: string) => void;
|
|
118
118
|
setGuardianContext: (ctx: unknown) => void;
|
|
119
|
+
setAuthContext: (ctx: unknown) => void;
|
|
119
120
|
setCommandIntent: (intent: unknown) => void;
|
|
120
121
|
updateClient: (
|
|
121
122
|
sendToClient: (msg: ServerMessage) => void,
|
|
@@ -180,6 +181,7 @@ function makeSession(overrides: Partial<TestSession> = {}): TestSession {
|
|
|
180
181
|
setTurnInterfaceContext: () => {},
|
|
181
182
|
setAssistantId: () => {},
|
|
182
183
|
setGuardianContext: () => {},
|
|
184
|
+
setAuthContext: () => {},
|
|
183
185
|
setCommandIntent: () => {},
|
|
184
186
|
updateClient: () => {},
|
|
185
187
|
emitActivityState: () => {},
|
|
@@ -66,6 +66,13 @@ mock.module("../providers/registry.js", () => ({
|
|
|
66
66
|
initializeProviders: () => {},
|
|
67
67
|
}));
|
|
68
68
|
|
|
69
|
+
// Mock token service — triggerGatewayReconcile now uses mintDaemonDeliveryToken
|
|
70
|
+
// instead of readHttpToken for the Bearer token.
|
|
71
|
+
let mintedToken: string | null = null;
|
|
72
|
+
mock.module("../runtime/auth/token-service.js", () => ({
|
|
73
|
+
mintDaemonDeliveryToken: () => mintedToken ?? "test-delivery-token",
|
|
74
|
+
}));
|
|
75
|
+
|
|
69
76
|
import { handleIngressConfig } from "../daemon/handlers/config.js";
|
|
70
77
|
import type { HandlerContext } from "../daemon/handlers/shared.js";
|
|
71
78
|
import type {
|
|
@@ -121,6 +128,7 @@ describe("Ingress reconcile trigger in handleIngressConfig", () => {
|
|
|
121
128
|
beforeEach(() => {
|
|
122
129
|
rawConfigStore = {};
|
|
123
130
|
httpTokenValue = null;
|
|
131
|
+
mintedToken = null;
|
|
124
132
|
reconcileCalls = [];
|
|
125
133
|
fetchShouldFail = false;
|
|
126
134
|
|
|
@@ -186,7 +194,7 @@ describe("Ingress reconcile trigger in handleIngressConfig", () => {
|
|
|
186
194
|
|
|
187
195
|
// ── Token present/missing behavior ──────────────────────────────────────
|
|
188
196
|
|
|
189
|
-
test("
|
|
197
|
+
test("always triggers reconcile even when readHttpToken returns null (uses mintDaemonDeliveryToken)", async () => {
|
|
190
198
|
httpTokenValue = null;
|
|
191
199
|
|
|
192
200
|
const msg: IngressConfigRequest = {
|
|
@@ -206,12 +214,12 @@ describe("Ingress reconcile trigger in handleIngressConfig", () => {
|
|
|
206
214
|
const res = sent[0] as { type: string; success: boolean };
|
|
207
215
|
expect(res.success).toBe(true);
|
|
208
216
|
|
|
209
|
-
//
|
|
210
|
-
expect(reconcileCalls).toHaveLength(
|
|
217
|
+
// Reconcile is always triggered using mintDaemonDeliveryToken
|
|
218
|
+
expect(reconcileCalls).toHaveLength(1);
|
|
211
219
|
});
|
|
212
220
|
|
|
213
|
-
test("triggers reconcile
|
|
214
|
-
|
|
221
|
+
test("triggers reconcile with mintDaemonDeliveryToken bearer token", async () => {
|
|
222
|
+
mintedToken = "test-bearer-token";
|
|
215
223
|
|
|
216
224
|
const msg: IngressConfigRequest = {
|
|
217
225
|
type: "ingress_config",
|
|
@@ -144,7 +144,7 @@ describe("vellum mcp list", () => {
|
|
|
144
144
|
expect(stdout).toContain("stdio");
|
|
145
145
|
expect(stdout).toContain("npx -y some-mcp-server");
|
|
146
146
|
expect(stdout).toContain("low");
|
|
147
|
-
});
|
|
147
|
+
}, 15_000);
|
|
148
148
|
|
|
149
149
|
test("--json outputs valid JSON", () => {
|
|
150
150
|
writeConfig({
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
2
|
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const actualEnv = await import("../config/env.js");
|
|
5
|
+
mock.module("../config/env.js", () => ({
|
|
6
|
+
...actualEnv,
|
|
7
|
+
isHttpAuthDisabled: () => true,
|
|
8
|
+
isMonitoringEnabled: () => false,
|
|
9
|
+
}));
|
|
5
10
|
|
|
6
11
|
// ─── Mocks (must be before any imports that depend on them) ─────────────────
|
|
7
12
|
|
|
@@ -351,7 +356,9 @@ mock.module("../subagent/index.js", () => ({
|
|
|
351
356
|
|
|
352
357
|
// ── Mock IPC protocol helpers ──────────────────────────────────────────────
|
|
353
358
|
|
|
359
|
+
const actualIpcProtocol = await import("../daemon/ipc-protocol.js");
|
|
354
360
|
mock.module("../daemon/ipc-protocol.js", () => ({
|
|
361
|
+
...actualIpcProtocol,
|
|
355
362
|
normalizeThreadType: (t: string) => t ?? "primary",
|
|
356
363
|
}));
|
|
357
364
|
|
|
@@ -414,6 +421,7 @@ function createCtx(overrides?: Partial<HandlerContext>): {
|
|
|
414
421
|
setAssistantId: noop,
|
|
415
422
|
setChannelCapabilities: noop,
|
|
416
423
|
setGuardianContext: noop,
|
|
424
|
+
setAuthContext: noop,
|
|
417
425
|
setCommandIntent: noop,
|
|
418
426
|
updateClient: noop,
|
|
419
427
|
processMessage: async () => {},
|
|
@@ -108,6 +108,7 @@ function makeCompletingSession(): Session {
|
|
|
108
108
|
setChannelCapabilities: () => {},
|
|
109
109
|
setAssistantId: () => {},
|
|
110
110
|
setGuardianContext: () => {},
|
|
111
|
+
setAuthContext: () => {},
|
|
111
112
|
setCommandIntent: () => {},
|
|
112
113
|
setTurnChannelContext: () => {},
|
|
113
114
|
setTurnInterfaceContext: () => {},
|
|
@@ -160,6 +161,7 @@ function makeHangingSession(): Session {
|
|
|
160
161
|
setChannelCapabilities: () => {},
|
|
161
162
|
setAssistantId: () => {},
|
|
162
163
|
setGuardianContext: () => {},
|
|
164
|
+
setAuthContext: () => {},
|
|
163
165
|
setCommandIntent: () => {},
|
|
164
166
|
setTurnChannelContext: () => {},
|
|
165
167
|
setTurnInterfaceContext: () => {},
|
|
@@ -236,7 +238,11 @@ function makePendingApprovalSession(
|
|
|
236
238
|
},
|
|
237
239
|
setChannelCapabilities: () => {},
|
|
238
240
|
setAssistantId: () => {},
|
|
239
|
-
|
|
241
|
+
guardianContext: undefined as unknown,
|
|
242
|
+
setGuardianContext(this: { guardianContext: unknown }, ctx: unknown) {
|
|
243
|
+
this.guardianContext = ctx;
|
|
244
|
+
},
|
|
245
|
+
setAuthContext: () => {},
|
|
240
246
|
setCommandIntent: () => {},
|
|
241
247
|
setTurnChannelContext: () => {},
|
|
242
248
|
setTurnInterfaceContext: () => {},
|
|
@@ -290,7 +296,7 @@ describe("POST /v1/messages — queue-if-busy and hub publishing", () => {
|
|
|
290
296
|
createBinding({
|
|
291
297
|
assistantId: "self",
|
|
292
298
|
channel: "vellum",
|
|
293
|
-
guardianExternalUserId: "
|
|
299
|
+
guardianExternalUserId: "dev-bypass",
|
|
294
300
|
guardianDeliveryChatId: "vellum",
|
|
295
301
|
guardianPrincipalId: "test-principal-id",
|
|
296
302
|
});
|
|
@@ -37,6 +37,10 @@ mock.module("../util/platform.js", () => ({
|
|
|
37
37
|
readHttpToken: () => "runtime-token",
|
|
38
38
|
}));
|
|
39
39
|
|
|
40
|
+
mock.module("../runtime/auth/token-service.js", () => ({
|
|
41
|
+
mintDaemonDeliveryToken: () => "runtime-token",
|
|
42
|
+
}));
|
|
43
|
+
|
|
40
44
|
mock.module("../config/loader.js", () => ({
|
|
41
45
|
loadConfig: () => configState,
|
|
42
46
|
}));
|
|
@@ -72,6 +72,7 @@ const {
|
|
|
72
72
|
ensurePromptFiles,
|
|
73
73
|
stripCommentLines,
|
|
74
74
|
buildExternalCommsIdentitySection,
|
|
75
|
+
buildPhoneCallsRoutingSection,
|
|
75
76
|
} = await import("../config/system-prompt.js");
|
|
76
77
|
|
|
77
78
|
/** Strip the Configuration and Skills sections so base-prompt tests stay focused. */
|
|
@@ -224,6 +225,26 @@ describe("buildSystemPrompt", () => {
|
|
|
224
225
|
expect(section).toContain("Occasional variations are acceptable");
|
|
225
226
|
});
|
|
226
227
|
|
|
228
|
+
test("includes phone calls routing section", () => {
|
|
229
|
+
const result = buildSystemPrompt();
|
|
230
|
+
expect(result).toContain("## Routing: Phone Calls");
|
|
231
|
+
expect(result).toContain('skill: "phone-calls"');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("buildPhoneCallsRoutingSection returns section with expected content", () => {
|
|
235
|
+
const section = buildPhoneCallsRoutingSection();
|
|
236
|
+
expect(section).toContain("## Routing: Phone Calls");
|
|
237
|
+
expect(section).toContain("Trigger phrases");
|
|
238
|
+
expect(section).toContain("Exclusivity rules");
|
|
239
|
+
expect(section).toContain("phone-calls");
|
|
240
|
+
expect(section).toContain("Do NOT improvise Twilio setup instructions");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("phone calls routing section excluded from low tier", () => {
|
|
244
|
+
const result = buildSystemPrompt("low");
|
|
245
|
+
expect(result).not.toContain("## Routing: Phone Calls");
|
|
246
|
+
});
|
|
247
|
+
|
|
227
248
|
test("includes memory persistence section in high tier", () => {
|
|
228
249
|
const result = buildSystemPrompt("high");
|
|
229
250
|
expect(result).toContain("## Memory Persistence");
|
|
@@ -78,7 +78,9 @@ mock.module("../tools/network/script-proxy/index.js", () => ({
|
|
|
78
78
|
}));
|
|
79
79
|
|
|
80
80
|
mock.module("../util/platform.js", () => ({
|
|
81
|
+
getRootDir: () => "/tmp",
|
|
81
82
|
getDataDir: () => "/tmp",
|
|
83
|
+
getRootDir: () => "/tmp",
|
|
82
84
|
getSocketPath: () => "/tmp/vellum.sock",
|
|
83
85
|
}));
|
|
84
86
|
|
package/src/cli/mcp.ts
CHANGED
|
@@ -15,7 +15,7 @@ const log = getCliLogger('cli');
|
|
|
15
15
|
export const HEALTH_CHECK_TIMEOUT_MS = 10_000;
|
|
16
16
|
|
|
17
17
|
export async function checkServerHealth(serverId: string, config: McpServerConfig, timeoutMs = HEALTH_CHECK_TIMEOUT_MS): Promise<string> {
|
|
18
|
-
const client = new McpClient(serverId
|
|
18
|
+
const client = new McpClient(serverId);
|
|
19
19
|
try {
|
|
20
20
|
await Promise.race([
|
|
21
21
|
client.connect(config.transport),
|
|
@@ -73,40 +73,93 @@ export function registerMcpCommand(program: Command): void {
|
|
|
73
73
|
|
|
74
74
|
log.info(`${entries.length} MCP server(s) configured:\n`);
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
for (const [id, cfg] of entries) {
|
|
78
|
-
if (!cfg || typeof cfg !== 'object') {
|
|
79
|
-
log.info(` ${id} (invalid config — skipped)\n`);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const enabled = cfg.enabled !== false;
|
|
83
|
-
const transport = cfg.transport;
|
|
84
|
-
const risk = cfg.defaultRiskLevel ?? 'high';
|
|
76
|
+
const isTTY = process.stdout.isTTY;
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
if (isTTY) {
|
|
79
|
+
// TTY path: print placeholders, run health checks in parallel, update in-place with ANSI codes
|
|
80
|
+
let lineCount = 0;
|
|
81
|
+
const healthChecks: { id: string; cfg: McpServerConfig; statusLine: number }[] = [];
|
|
82
|
+
|
|
83
|
+
for (const [id, cfg] of entries) {
|
|
84
|
+
if (!cfg || typeof cfg !== 'object') {
|
|
85
|
+
log.info(` ${id} (invalid config — skipped)\n`);
|
|
86
|
+
lineCount += 2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const enabled = cfg.enabled !== false;
|
|
90
|
+
const transport = cfg.transport;
|
|
91
|
+
const risk = cfg.defaultRiskLevel ?? 'high';
|
|
92
|
+
const statusText = !enabled ? '✗ disabled' : '⏳ Checking...';
|
|
93
|
+
|
|
94
|
+
log.info(` ${id}`);
|
|
95
|
+
lineCount++;
|
|
96
|
+
const statusLine = lineCount;
|
|
97
|
+
log.info(` Status: ${statusText}`);
|
|
98
|
+
lineCount++;
|
|
99
|
+
log.info(` Transport: ${transport?.type ?? 'unknown'}`);
|
|
100
|
+
lineCount++;
|
|
101
|
+
if (transport?.type === 'stdio') {
|
|
102
|
+
log.info(` Command: ${transport.command} ${(transport.args ?? []).join(' ')}`);
|
|
103
|
+
lineCount++;
|
|
104
|
+
} else if (transport && 'url' in transport) {
|
|
105
|
+
log.info(` URL: ${transport.url}`);
|
|
106
|
+
lineCount++;
|
|
107
|
+
}
|
|
108
|
+
log.info(` Risk: ${risk}`);
|
|
109
|
+
lineCount++;
|
|
110
|
+
if (cfg.allowedTools) { log.info(` Allowed: ${cfg.allowedTools.join(', ')}`); lineCount++; }
|
|
111
|
+
if (cfg.blockedTools) { log.info(` Blocked: ${cfg.blockedTools.join(', ')}`); lineCount++; }
|
|
112
|
+
log.info('');
|
|
113
|
+
lineCount++;
|
|
114
|
+
|
|
115
|
+
if (enabled) {
|
|
116
|
+
healthChecks.push({ id, cfg, statusLine });
|
|
117
|
+
}
|
|
92
118
|
}
|
|
93
119
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
if (healthChecks.length === 0) return;
|
|
121
|
+
|
|
122
|
+
// Run health checks in parallel, update status lines in-place with ANSI codes
|
|
123
|
+
await Promise.all(healthChecks.map(async ({ id, cfg, statusLine }) => {
|
|
124
|
+
const health = await checkServerHealth(id, cfg);
|
|
125
|
+
const up = lineCount - statusLine;
|
|
126
|
+
process.stdout.write(`\x1b[${up}A\r\x1b[2K Status: ${health}\x1b[${up}B\r`);
|
|
127
|
+
}));
|
|
128
|
+
} else {
|
|
129
|
+
// Non-TTY path: run health checks sequentially, print final status directly (no ANSI codes)
|
|
130
|
+
for (const [id, cfg] of entries) {
|
|
131
|
+
if (!cfg || typeof cfg !== 'object') {
|
|
132
|
+
log.info(` ${id} (invalid config — skipped)\n`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const enabled = cfg.enabled !== false;
|
|
136
|
+
const transport = cfg.transport;
|
|
137
|
+
const risk = cfg.defaultRiskLevel ?? 'high';
|
|
138
|
+
|
|
139
|
+
let statusText: string;
|
|
140
|
+
if (!enabled) {
|
|
141
|
+
statusText = '✗ disabled';
|
|
142
|
+
} else {
|
|
143
|
+
statusText = await checkServerHealth(id, cfg);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
log.info(` ${id}`);
|
|
147
|
+
log.info(` Status: ${statusText}`);
|
|
148
|
+
log.info(` Transport: ${transport?.type ?? 'unknown'}`);
|
|
149
|
+
if (transport?.type === 'stdio') {
|
|
150
|
+
log.info(` Command: ${transport.command} ${(transport.args ?? []).join(' ')}`);
|
|
151
|
+
} else if (transport && 'url' in transport) {
|
|
152
|
+
log.info(` URL: ${transport.url}`);
|
|
153
|
+
}
|
|
154
|
+
log.info(` Risk: ${risk}`);
|
|
155
|
+
if (cfg.allowedTools) { log.info(` Allowed: ${cfg.allowedTools.join(', ')}`); }
|
|
156
|
+
if (cfg.blockedTools) { log.info(` Blocked: ${cfg.blockedTools.join(', ')}`); }
|
|
157
|
+
log.info('');
|
|
101
158
|
}
|
|
102
|
-
log.info(` Risk: ${risk}`);
|
|
103
|
-
if (cfg.allowedTools) log.info(` Allowed: ${cfg.allowedTools.join(', ')}`);
|
|
104
|
-
if (cfg.blockedTools) log.info(` Blocked: ${cfg.blockedTools.join(', ')}`);
|
|
105
|
-
log.info('');
|
|
106
159
|
}
|
|
107
160
|
|
|
108
161
|
// Health checks may leave MCP transports alive — force exit
|
|
109
|
-
|
|
162
|
+
process.exit(0);
|
|
110
163
|
});
|
|
111
164
|
|
|
112
165
|
mcp
|
|
@@ -11,7 +11,7 @@ You are helping your user set up guardian verification for a messaging channel (
|
|
|
11
11
|
|
|
12
12
|
- Use the injected `INTERNAL_GATEWAY_BASE_URL` for gateway API calls.
|
|
13
13
|
- Never call the daemon runtime port directly; always call the gateway URL.
|
|
14
|
-
- The bearer token is
|
|
14
|
+
- The bearer token is available as the `$GATEWAY_AUTH_TOKEN` environment variable.
|
|
15
15
|
- Run shell commands for this skill with `host_bash` (not sandbox `bash`) so host auth/token and gateway routing are reliable.
|
|
16
16
|
- Keep narration minimal: execute required calls first, then provide a concise status update. Do not narrate internal install/check/load chatter unless something fails.
|
|
17
17
|
|
|
@@ -39,10 +39,9 @@ Based on the chosen channel, ask for the required destination:
|
|
|
39
39
|
Execute the outbound start request:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
43
42
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/outbound/start" \
|
|
44
43
|
-H "Content-Type: application/json" \
|
|
45
|
-
-H "Authorization: Bearer $
|
|
44
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
46
45
|
-d '{"channel": "<channel>", "destination": "<destination>"}'
|
|
47
46
|
```
|
|
48
47
|
|
|
@@ -77,10 +76,9 @@ Handle each error code:
|
|
|
77
76
|
If the user says they did not receive the code or asks to resend:
|
|
78
77
|
|
|
79
78
|
```bash
|
|
80
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
81
79
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/outbound/resend" \
|
|
82
80
|
-H "Content-Type: application/json" \
|
|
83
|
-
-H "Authorization: Bearer $
|
|
81
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
84
82
|
-d '{"channel": "<channel>"}'
|
|
85
83
|
```
|
|
86
84
|
|
|
@@ -107,10 +105,9 @@ Handle each error code from the resend endpoint:
|
|
|
107
105
|
If the user wants to cancel the verification:
|
|
108
106
|
|
|
109
107
|
```bash
|
|
110
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
111
108
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/outbound/cancel" \
|
|
112
109
|
-H "Content-Type: application/json" \
|
|
113
|
-
-H "Authorization: Bearer $
|
|
110
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
114
111
|
-d '{"channel": "<channel>"}'
|
|
115
112
|
```
|
|
116
113
|
|
|
@@ -126,9 +123,8 @@ For **voice** verification only: after telling the user their code and instructi
|
|
|
126
123
|
2. Check the binding status:
|
|
127
124
|
|
|
128
125
|
```bash
|
|
129
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
130
126
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/status?channel=voice" \
|
|
131
|
-
-H "Authorization: Bearer $
|
|
127
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
132
128
|
```
|
|
133
129
|
|
|
134
130
|
3. If the response shows `bound: true`: immediately send a proactive success message in the current chat — "Voice verification complete! Your phone number is now the trusted guardian." Stop polling.
|
|
@@ -153,9 +149,8 @@ When in a **rebind flow** (i.e., the `start_outbound` request included `"rebind"
|
|
|
153
149
|
After the user reports entering the code, verify the binding was created:
|
|
154
150
|
|
|
155
151
|
```bash
|
|
156
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
157
152
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/status?channel=<channel>" \
|
|
158
|
-
-H "Authorization: Bearer $
|
|
153
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
159
154
|
```
|
|
160
155
|
|
|
161
156
|
If the response shows the guardian is bound, confirm success: "Guardian verified! Your [channel] identity is now the trusted guardian."
|
|
@@ -49,9 +49,8 @@ The user's assistant gets its own personal phone number through Twilio. All impl
|
|
|
49
49
|
Check whether Twilio credentials, phone number, and public ingress are already configured:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
53
52
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
|
|
54
|
-
-H "Authorization: Bearer $
|
|
53
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
55
54
|
```
|
|
56
55
|
|
|
57
56
|
```bash
|
|
@@ -12,9 +12,8 @@ You are helping your user set up SMS messaging. This skill orchestrates Twilio s
|
|
|
12
12
|
First, check the current SMS channel readiness state via the gateway:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
16
15
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
|
|
17
|
-
-H "Authorization: Bearer $
|
|
16
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
18
17
|
```
|
|
19
18
|
|
|
20
19
|
Inspect the response for `hasCredentials` and `phoneNumber`.
|
|
@@ -35,9 +34,8 @@ Tell the user: _"SMS needs Twilio configured first. I've loaded the Twilio setup
|
|
|
35
34
|
After twilio-setup completes, re-check readiness by calling the config endpoint again:
|
|
36
35
|
|
|
37
36
|
```bash
|
|
38
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
39
37
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
|
|
40
|
-
-H "Authorization: Bearer $
|
|
38
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
41
39
|
```
|
|
42
40
|
|
|
43
41
|
If baseline is still not ready, report the specific failures and ask the user to address them before continuing.
|
|
@@ -47,9 +45,8 @@ If baseline is still not ready, report the specific failures and ask the user to
|
|
|
47
45
|
Once baseline is ready, check SMS compliance status including remote (Twilio API) checks:
|
|
48
46
|
|
|
49
47
|
```bash
|
|
50
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
51
48
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/sms/compliance" \
|
|
52
|
-
-H "Authorization: Bearer $
|
|
49
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
53
50
|
```
|
|
54
51
|
|
|
55
52
|
Examine the compliance results:
|
|
@@ -90,9 +87,8 @@ The `tollfreePhoneNumberSid` is returned by the compliance status response in th
|
|
|
90
87
|
**Step 3c: Submit verification:**
|
|
91
88
|
|
|
92
89
|
```bash
|
|
93
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
94
90
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/sms/compliance/tollfree" \
|
|
95
|
-
-H "Authorization: Bearer $
|
|
91
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
96
92
|
-H "Content-Type: application/json" \
|
|
97
93
|
-d '{
|
|
98
94
|
"tollfreePhoneNumberSid": "<compliance.tollfreePhoneNumberSid from Step 3a>",
|
|
@@ -118,9 +114,8 @@ The endpoint validates all fields before submitting to Twilio and returns clear
|
|
|
118
114
|
**Step 3d: Update a rejected verification** (if `editAllowed` is true):
|
|
119
115
|
|
|
120
116
|
```bash
|
|
121
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
122
117
|
curl -s -X PATCH "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/sms/compliance/tollfree/<verificationSid>" \
|
|
123
|
-
-H "Authorization: Bearer $
|
|
118
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
124
119
|
-H "Content-Type: application/json" \
|
|
125
120
|
-d '{
|
|
126
121
|
"businessName": "updated value",
|
|
@@ -133,9 +128,8 @@ Only include fields that need to change. The endpoint checks edit eligibility an
|
|
|
133
128
|
**Step 3e: Delete and resubmit** (if editing is not allowed):
|
|
134
129
|
|
|
135
130
|
```bash
|
|
136
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
137
131
|
curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/sms/compliance/tollfree/<verificationSid>" \
|
|
138
|
-
-H "Authorization: Bearer $
|
|
132
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
139
133
|
```
|
|
140
134
|
|
|
141
135
|
After deletion, return to Step 3b to collect information and resubmit. Warn the user that deleting resets their position in the review queue.
|
|
@@ -183,9 +177,8 @@ Tell the user: _"Let's send a test SMS to verify everything works. What phone nu
|
|
|
183
177
|
After the user provides a number, send a test message via the gateway:
|
|
184
178
|
|
|
185
179
|
```bash
|
|
186
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
187
180
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/sms/test" \
|
|
188
|
-
-H "Authorization: Bearer $
|
|
181
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
189
182
|
-H "Content-Type: application/json" \
|
|
190
183
|
-d '{"phoneNumber":"<recipient phone number>","text":"Test SMS from your Vellum assistant."}'
|
|
191
184
|
```
|
|
@@ -200,9 +193,8 @@ Report the result honestly:
|
|
|
200
193
|
If the test fails or the user reports SMS issues, run the SMS doctor:
|
|
201
194
|
|
|
202
195
|
```bash
|
|
203
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
204
196
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/sms/doctor" \
|
|
205
|
-
-H "Authorization: Bearer $
|
|
197
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
206
198
|
```
|
|
207
199
|
|
|
208
200
|
This runs a comprehensive health diagnostic, checking channel readiness, compliance/toll-free verification status, and the last test result. Report the diagnostics and actionable items to the user.
|
|
@@ -39,7 +39,7 @@ After the token is collected, call the composite setup endpoint which validates
|
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
curl -sf -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/telegram/setup" \
|
|
42
|
-
-H "Authorization: Bearer $
|
|
42
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
43
43
|
-H "Content-Type: application/json" \
|
|
44
44
|
-d '{}'
|
|
45
45
|
```
|
|
@@ -101,7 +101,7 @@ Before reporting success, confirm the guardian binding was actually created. Che
|
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
103
|
curl -sf "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/status?channel=telegram" \
|
|
104
|
-
-H "Authorization: Bearer $
|
|
104
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
If the binding is absent and the user said they completed the verification:
|
|
@@ -120,7 +120,7 @@ Summarize what was done:
|
|
|
120
120
|
- Guardian identity: {verified | not configured}
|
|
121
121
|
- Guardian verification status: {verified via outbound flow | skipped}
|
|
122
122
|
- Routing configuration validated
|
|
123
|
-
- To re-check guardian status later, use: `curl -sf "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/status?channel=telegram" -H "Authorization: Bearer $
|
|
123
|
+
- To re-check guardian status later, use: `curl -sf "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/status?channel=telegram" -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"`
|
|
124
124
|
|
|
125
125
|
The gateway automatically detects credentials from the vault, reconciles the Telegram webhook registration, and begins accepting Telegram webhooks shortly. In single-assistant mode, routing is automatically configured — no manual environment variable configuration or webhook registration is needed. If the webhook secret changes later, the gateway's credential watcher will automatically re-register the webhook. If the ingress URL changes (e.g., tunnel restart), the assistant daemon triggers an immediate internal reconcile so the webhook re-registers automatically without a gateway restart.
|
|
126
126
|
|
|
@@ -11,7 +11,7 @@ You are helping your user manage trusted contacts and invite links for the Vellu
|
|
|
11
11
|
|
|
12
12
|
- Use the injected `INTERNAL_GATEWAY_BASE_URL` for gateway API calls.
|
|
13
13
|
- Use gateway control-plane routes only: this skill calls `/v1/ingress/*` and `/v1/integrations/telegram/config` on the gateway, never the daemon runtime port directly.
|
|
14
|
-
- The bearer token is
|
|
14
|
+
- The bearer token is available as the `$GATEWAY_AUTH_TOKEN` environment variable.
|
|
15
15
|
|
|
16
16
|
## Concepts
|
|
17
17
|
|
|
@@ -29,9 +29,8 @@ You are helping your user manage trusted contacts and invite links for the Vellu
|
|
|
29
29
|
Use this to show the user who currently has access, or to look up a specific contact.
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
33
32
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members" \
|
|
34
|
-
-H "Authorization: Bearer $
|
|
33
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
35
34
|
```
|
|
36
35
|
|
|
37
36
|
Optional query parameters for filtering:
|
|
@@ -42,7 +41,7 @@ Optional query parameters for filtering:
|
|
|
42
41
|
Example with filters:
|
|
43
42
|
```bash
|
|
44
43
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members?sourceChannel=telegram&status=active" \
|
|
45
|
-
-H "Authorization: Bearer $
|
|
44
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
46
45
|
```
|
|
47
46
|
|
|
48
47
|
The response contains `{ ok: true, members: [...] }` where each member has:
|
|
@@ -65,10 +64,9 @@ Use this when the user wants to grant someone access to message the assistant. *
|
|
|
65
64
|
Ask the user: *"I'll add [name/identifier] on [channel] as an allowed contact. Should I proceed?"*
|
|
66
65
|
|
|
67
66
|
```bash
|
|
68
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
69
67
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members" \
|
|
70
68
|
-H "Content-Type: application/json" \
|
|
71
|
-
-H "Authorization: Bearer $
|
|
69
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
72
70
|
-d '{
|
|
73
71
|
"sourceChannel": "<channel>",
|
|
74
72
|
"externalUserId": "<user_id>",
|
|
@@ -97,9 +95,8 @@ Ask the user: *"I'll revoke access for [name/identifier]. They will no longer be
|
|
|
97
95
|
First, list members to find the member's `id`, then revoke:
|
|
98
96
|
|
|
99
97
|
```bash
|
|
100
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
101
98
|
curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members/<member_id>" \
|
|
102
|
-
-H "Authorization: Bearer $
|
|
99
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
103
100
|
-H "Content-Type: application/json" \
|
|
104
101
|
-d '{"reason": "<optional reason>"}'
|
|
105
102
|
```
|
|
@@ -113,10 +110,9 @@ Use this when the user wants to explicitly block someone. Blocking is stronger t
|
|
|
113
110
|
Ask the user: *"I'll block [name/identifier]. They will be permanently denied from messaging the assistant. Should I proceed?"*
|
|
114
111
|
|
|
115
112
|
```bash
|
|
116
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
117
113
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members/<member_id>/block" \
|
|
118
114
|
-H "Content-Type: application/json" \
|
|
119
|
-
-H "Authorization: Bearer $
|
|
115
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
120
116
|
-d '{"reason": "<optional reason>"}'
|
|
121
117
|
```
|
|
122
118
|
|
|
@@ -127,11 +123,9 @@ Use this when the guardian wants to invite someone to message the assistant on T
|
|
|
127
123
|
**Important**: The shell snippet below emits a `<vellum-sensitive-output>` directive containing the raw invite token. The tool executor automatically strips this directive and replaces the raw token with a placeholder so the LLM never sees it. The placeholder is resolved back to the real token in the final assistant reply.
|
|
128
124
|
|
|
129
125
|
```bash
|
|
130
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
131
|
-
|
|
132
126
|
INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
|
|
133
127
|
-H "Content-Type: application/json" \
|
|
134
|
-
-H "Authorization: Bearer $
|
|
128
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
135
129
|
-d '{
|
|
136
130
|
"sourceChannel": "telegram",
|
|
137
131
|
"maxUses": 1,
|
|
@@ -160,7 +154,7 @@ fi
|
|
|
160
154
|
# Prefer backend-provided canonical link when available.
|
|
161
155
|
if [ -z "$INVITE_URL" ]; then
|
|
162
156
|
BOT_CONFIG_JSON=$(curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/telegram/config" \
|
|
163
|
-
-H "Authorization: Bearer $
|
|
157
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN")
|
|
164
158
|
BOT_USERNAME=$(printf '%s' "$BOT_CONFIG_JSON" | tr -d '\n' | sed -n 's/.*"botUsername"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
165
159
|
if [ -z "$BOT_USERNAME" ]; then
|
|
166
160
|
echo "error:no_share_url_or_bot_username"
|
|
@@ -201,9 +195,8 @@ If the Telegram bot username is not available (integration not set up), tell the
|
|
|
201
195
|
Use this to show the guardian their active (and optionally all) invite links.
|
|
202
196
|
|
|
203
197
|
```bash
|
|
204
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
205
198
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites?sourceChannel=telegram" \
|
|
206
|
-
-H "Authorization: Bearer $
|
|
199
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
207
200
|
```
|
|
208
201
|
|
|
209
202
|
Optional query parameters:
|
|
@@ -232,9 +225,8 @@ Ask the user: *"I'll revoke the invite link [note or ID]. It will no longer be u
|
|
|
232
225
|
First, list invites to find the invite's `id`, then revoke:
|
|
233
226
|
|
|
234
227
|
```bash
|
|
235
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
236
228
|
curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites/<invite_id>" \
|
|
237
|
-
-H "Authorization: Bearer $
|
|
229
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
238
230
|
```
|
|
239
231
|
|
|
240
232
|
Replace `<invite_id>` with the invite's `id` from the list response.
|
|
@@ -246,11 +238,9 @@ Use this when the guardian wants to authorize a specific phone number to call th
|
|
|
246
238
|
**Important**: The response includes a `voiceCode` field that is only returned at creation time and cannot be retrieved later. Extract and present it clearly.
|
|
247
239
|
|
|
248
240
|
```bash
|
|
249
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
250
|
-
|
|
251
241
|
INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
|
|
252
242
|
-H "Content-Type: application/json" \
|
|
253
|
-
-H "Authorization: Bearer $
|
|
243
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
254
244
|
-d '{
|
|
255
245
|
"sourceChannel": "voice",
|
|
256
246
|
"expectedExternalUserId": "<phone_number_E164>",
|
|
@@ -303,9 +293,8 @@ If the user provides a phone number without the `+` country code prefix, ask the
|
|
|
303
293
|
Use this to show the guardian their active voice invites.
|
|
304
294
|
|
|
305
295
|
```bash
|
|
306
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
307
296
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites?sourceChannel=voice" \
|
|
308
|
-
-H "Authorization: Bearer $
|
|
297
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
309
298
|
```
|
|
310
299
|
|
|
311
300
|
Optional query parameters:
|
|
@@ -327,9 +316,8 @@ Ask the user: *"I'll revoke the voice invite for [phone number or note]. The cod
|
|
|
327
316
|
First, list voice invites to find the invite's `id`, then revoke:
|
|
328
317
|
|
|
329
318
|
```bash
|
|
330
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
331
319
|
curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites/<invite_id>" \
|
|
332
|
-
-H "Authorization: Bearer $
|
|
320
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
333
321
|
```
|
|
334
322
|
|
|
335
323
|
Replace `<invite_id>` with the invite's `id` from the list response. The same revoke endpoint is used for both Telegram and voice invites.
|
|
@@ -11,17 +11,16 @@ You are helping your user configure Twilio for voice calls and SMS messaging. Tw
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
15
14
|
# 1. Check current status
|
|
16
15
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
|
|
17
|
-
-H "Authorization: Bearer $
|
|
16
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
18
17
|
# 2. Store credentials (after collecting via credential_store prompt)
|
|
19
18
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/credentials" \
|
|
20
|
-
-H "Authorization: Bearer $
|
|
19
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" -H "Content-Type: application/json" \
|
|
21
20
|
-d '{"accountSid":"ACxxx","authToken":"xxx"}'
|
|
22
21
|
# 3. Provision or assign a number
|
|
23
22
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/numbers/provision" \
|
|
24
|
-
-H "Authorization: Bearer $
|
|
23
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" -H "Content-Type: application/json" \
|
|
25
24
|
-d '{"country":"US","areaCode":"415"}'
|
|
26
25
|
```
|
|
27
26
|
|
|
@@ -67,9 +66,8 @@ All HTTP examples below include the optional `assistantId` query parameter in as
|
|
|
67
66
|
First, check whether Twilio is already configured:
|
|
68
67
|
|
|
69
68
|
```bash
|
|
70
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
71
69
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
|
|
72
|
-
-H "Authorization: Bearer $
|
|
70
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
73
71
|
```
|
|
74
72
|
|
|
75
73
|
The response includes:
|
|
@@ -96,9 +94,8 @@ If credentials are not yet stored, guide the user through Twilio account setup:
|
|
|
96
94
|
After both credentials are collected, retrieve them from secure storage and send them to the gateway:
|
|
97
95
|
|
|
98
96
|
```bash
|
|
99
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
100
97
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/credentials" \
|
|
101
|
-
-H "Authorization: Bearer $
|
|
98
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
102
99
|
-H "Content-Type: application/json" \
|
|
103
100
|
-d '{"accountSid":"<value from credential_store for twilio/account_sid>","authToken":"<value from credential_store for twilio/auth_token>"}'
|
|
104
101
|
```
|
|
@@ -116,9 +113,8 @@ The assistant needs a phone number to make calls and send SMS. There are two pat
|
|
|
116
113
|
If the user wants to buy a new number through Twilio:
|
|
117
114
|
|
|
118
115
|
```bash
|
|
119
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
120
116
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/numbers/provision" \
|
|
121
|
-
-H "Authorization: Bearer $
|
|
117
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
122
118
|
-H "Content-Type: application/json" \
|
|
123
119
|
-d '{"country":"US","areaCode":"415"}'
|
|
124
120
|
```
|
|
@@ -143,9 +139,8 @@ If ingress is not yet configured, webhook setup is skipped gracefully — the nu
|
|
|
143
139
|
If the user already has a Twilio phone number, first list available numbers:
|
|
144
140
|
|
|
145
141
|
```bash
|
|
146
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
147
142
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/numbers" \
|
|
148
|
-
-H "Authorization: Bearer $
|
|
143
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
149
144
|
```
|
|
150
145
|
|
|
151
146
|
The response includes a `numbers` array with each number's `phoneNumber`, `friendlyName`, and `capabilities` (voice, SMS). Present these to the user and let them choose.
|
|
@@ -153,9 +148,8 @@ The response includes a `numbers` array with each number's `phoneNumber`, `frien
|
|
|
153
148
|
Then assign the chosen number:
|
|
154
149
|
|
|
155
150
|
```bash
|
|
156
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
157
151
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/numbers/assign" \
|
|
158
|
-
-H "Authorization: Bearer $
|
|
152
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
159
153
|
-H "Content-Type: application/json" \
|
|
160
154
|
-d '{"phoneNumber":"+14155551234"}'
|
|
161
155
|
```
|
|
@@ -173,9 +167,8 @@ credential_store action=store service=twilio field=phone_number value=+141555512
|
|
|
173
167
|
Then assign it through the gateway:
|
|
174
168
|
|
|
175
169
|
```bash
|
|
176
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
177
170
|
curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/numbers/assign" \
|
|
178
|
-
-H "Authorization: Bearer $
|
|
171
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
179
172
|
-H "Content-Type: application/json" \
|
|
180
173
|
-d '{"phoneNumber":"+14155551234"}'
|
|
181
174
|
```
|
|
@@ -211,9 +204,8 @@ Webhook URLs are automatically configured on the Twilio phone number when provis
|
|
|
211
204
|
After configuration, verify by checking the config endpoint again.
|
|
212
205
|
|
|
213
206
|
```bash
|
|
214
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
215
207
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
|
|
216
|
-
-H "Authorization: Bearer $
|
|
208
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
217
209
|
```
|
|
218
210
|
|
|
219
211
|
Confirm:
|
|
@@ -251,13 +243,12 @@ After the guardian-verify-setup skill completes verification for a channel, load
|
|
|
251
243
|
To re-check guardian status later, query the channel(s) that were verified:
|
|
252
244
|
|
|
253
245
|
```bash
|
|
254
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
255
246
|
# Check SMS guardian status
|
|
256
247
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/status?channel=sms" \
|
|
257
|
-
-H "Authorization: Bearer $
|
|
248
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
258
249
|
# Check voice guardian status
|
|
259
250
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/status?channel=voice" \
|
|
260
|
-
-H "Authorization: Bearer $
|
|
251
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
261
252
|
```
|
|
262
253
|
|
|
263
254
|
Check the status for whichever channel(s) the user actually verified (SMS, voice, or both). Report the guardian verification result per channel: **"Guardian identity — SMS: {verified | not configured}, Voice: {verified | not configured}."**
|
|
@@ -280,9 +271,8 @@ SMS is available automatically once Twilio is configured — no additional featu
|
|
|
280
271
|
If the user wants to disconnect Twilio:
|
|
281
272
|
|
|
282
273
|
```bash
|
|
283
|
-
TOKEN=$(cat ~/.vellum/http-token)
|
|
284
274
|
curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/credentials" \
|
|
285
|
-
-H "Authorization: Bearer $
|
|
275
|
+
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
|
|
286
276
|
```
|
|
287
277
|
|
|
288
278
|
This removes the stored Account SID and Auth Token. Phone number assignments are preserved. Voice calls and SMS will stop working until credentials are reconfigured.
|
|
@@ -145,6 +145,7 @@ export function buildSystemPrompt(tier: ResponseTier = 'high'): string {
|
|
|
145
145
|
parts.push(buildAttachmentSection());
|
|
146
146
|
parts.push(buildInChatConfigurationSection());
|
|
147
147
|
parts.push(buildVoiceSetupRoutingSection());
|
|
148
|
+
parts.push(buildPhoneCallsRoutingSection());
|
|
148
149
|
parts.push(buildChannelCommandIntentSection());
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -373,6 +374,37 @@ export function buildVoiceSetupRoutingSection(): string {
|
|
|
373
374
|
].join('\n');
|
|
374
375
|
}
|
|
375
376
|
|
|
377
|
+
export function buildPhoneCallsRoutingSection(): string {
|
|
378
|
+
return [
|
|
379
|
+
'## Routing: Phone Calls',
|
|
380
|
+
'',
|
|
381
|
+
'When the user asks to set up phone calling, place a call, configure Twilio for voice, or anything related to outbound/inbound phone calls, load the **Phone Calls** skill.',
|
|
382
|
+
'',
|
|
383
|
+
'### Trigger phrases',
|
|
384
|
+
'- "Set up phone calling" / "enable calls"',
|
|
385
|
+
'- "Make a call to..." / "call [number/business]"',
|
|
386
|
+
'- "Configure Twilio" (in context of voice calls, not SMS)',
|
|
387
|
+
'- "Can you make phone calls?"',
|
|
388
|
+
'- "Set up my phone number" (for calling, not SMS)',
|
|
389
|
+
'',
|
|
390
|
+
'### What it does',
|
|
391
|
+
'The skill handles the full phone calling lifecycle:',
|
|
392
|
+
'1. Twilio credential setup (delegates to twilio-setup skill)',
|
|
393
|
+
'2. Public ingress configuration (delegates to public-ingress skill)',
|
|
394
|
+
'3. Enabling the calls feature',
|
|
395
|
+
'4. Placing outbound calls and receiving inbound calls',
|
|
396
|
+
'5. Voice quality configuration (standard Twilio TTS or ElevenLabs)',
|
|
397
|
+
'',
|
|
398
|
+
'Load with: `skill_load` using `skill: "phone-calls"`',
|
|
399
|
+
'',
|
|
400
|
+
'### Exclusivity rules',
|
|
401
|
+
'- Do NOT improvise Twilio setup instructions from general knowledge — always load the skill first.',
|
|
402
|
+
'- Do NOT confuse with voice-setup (local PTT/wake word/microphone) or guardian-verify-setup (channel verification).',
|
|
403
|
+
'- If the user says "voice" in the context of phone calls or Twilio, load phone-calls, not voice-setup.',
|
|
404
|
+
'- For guardian voice verification specifically, load guardian-verify-setup instead.',
|
|
405
|
+
].join('\n');
|
|
406
|
+
}
|
|
407
|
+
|
|
376
408
|
function buildToolPermissionSection(): string {
|
|
377
409
|
return [
|
|
378
410
|
'## Tool Permissions',
|
package/src/mcp/client.ts
CHANGED
|
@@ -30,15 +30,13 @@ export class McpClient {
|
|
|
30
30
|
private transport: StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport | null = null;
|
|
31
31
|
private connected = false;
|
|
32
32
|
private oauthProvider: McpOAuthProvider | null = null;
|
|
33
|
-
private quiet: boolean;
|
|
34
33
|
|
|
35
34
|
get isConnected(): boolean {
|
|
36
35
|
return this.connected;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
constructor(serverId: string
|
|
38
|
+
constructor(serverId: string) {
|
|
40
39
|
this.serverId = serverId;
|
|
41
|
-
this.quiet = opts?.quiet ?? false;
|
|
42
40
|
this.client = new Client({
|
|
43
41
|
name: 'vellum-assistant',
|
|
44
42
|
version: '1.0.0',
|
|
@@ -61,7 +59,7 @@ export class McpClient {
|
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
log.info({ serverId: this.serverId }, 'Connecting to MCP server');
|
|
65
63
|
this.transport = this.createTransport(transportConfig);
|
|
66
64
|
|
|
67
65
|
try {
|
|
@@ -82,13 +80,11 @@ export class McpClient {
|
|
|
82
80
|
if (isAuthError) {
|
|
83
81
|
// Auth-related — user can run `vellum mcp auth <name>` to authenticate.
|
|
84
82
|
log.info({ serverId: this.serverId, err }, 'MCP server requires authentication');
|
|
85
|
-
if (!this.quiet) console.log(`[MCP] Server "${this.serverId}" requires authentication. Run "vellum mcp auth ${this.serverId}" to authenticate.`);
|
|
86
83
|
return;
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
// Non-auth error (DNS, TLS, timeout, etc.) — log and re-throw
|
|
90
87
|
log.error({ serverId: this.serverId, err }, 'MCP server connection failed');
|
|
91
|
-
if (!this.quiet) console.error(`[MCP] Server "${this.serverId}" connection failed: ${err instanceof Error ? err.message : err}`);
|
|
92
88
|
throw err;
|
|
93
89
|
}
|
|
94
90
|
|
|
@@ -96,7 +92,6 @@ export class McpClient {
|
|
|
96
92
|
}
|
|
97
93
|
|
|
98
94
|
this.connected = true;
|
|
99
|
-
if (!this.quiet) console.log(`[MCP] Server "${this.serverId}" connected successfully`);
|
|
100
95
|
log.info({ serverId: this.serverId }, 'MCP client connected');
|
|
101
96
|
}
|
|
102
97
|
|
|
@@ -148,11 +148,19 @@ export function getSecureKey(account: string): string | undefined {
|
|
|
148
148
|
* Returns `true` on success, `false` on failure.
|
|
149
149
|
*/
|
|
150
150
|
export function setSecureKey(account: string, value: string): boolean {
|
|
151
|
-
|
|
151
|
+
const result = withKeychainFallback(
|
|
152
152
|
() => keychain.setKey(account, value),
|
|
153
153
|
() => encryptedStore.setKey(account, value),
|
|
154
154
|
false,
|
|
155
155
|
);
|
|
156
|
+
// When writing to the encrypted store after a keychain downgrade, clean up
|
|
157
|
+
// any stale keychain entry so the gateway's credential-reader (which tries
|
|
158
|
+
// keychain first) does not read an outdated value.
|
|
159
|
+
if (result && downgradedFromKeychain && getBackend() === "encrypted") {
|
|
160
|
+
keychainMissCache.delete(account);
|
|
161
|
+
try { keychain.deleteKey(account); } catch { /* best-effort */ }
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
/**
|
|
@@ -278,7 +286,15 @@ export async function setSecureKeyAsync(
|
|
|
278
286
|
value: string,
|
|
279
287
|
): Promise<boolean> {
|
|
280
288
|
const backend = await getBackendAsync();
|
|
281
|
-
if (backend === "encrypted")
|
|
289
|
+
if (backend === "encrypted") {
|
|
290
|
+
const result = encryptedStore.setKey(account, value);
|
|
291
|
+
// Clean up stale keychain entry (mirrors setSecureKey logic).
|
|
292
|
+
if (result && downgradedFromKeychain) {
|
|
293
|
+
keychainMissCache.delete(account);
|
|
294
|
+
try { await keychain.deleteKeyAsync(account); } catch { /* best-effort */ }
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
282
298
|
if (backend !== "keychain") return false;
|
|
283
299
|
|
|
284
300
|
const result = await keychain.setKeyAsync(account, value);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Shared by the sandbox bash tool and skill sandbox runner.
|
|
7
7
|
*/
|
|
8
8
|
import { getGatewayInternalBaseUrl, getIngressPublicBaseUrl } from '../../config/env.js';
|
|
9
|
+
import { isSigningKeyInitialized, mintEdgeRelayToken } from '../../runtime/auth/token-service.js';
|
|
9
10
|
|
|
10
11
|
const SAFE_ENV_VARS = [
|
|
11
12
|
'PATH',
|
|
@@ -42,5 +43,11 @@ export function buildSanitizedEnv(): Record<string, string> {
|
|
|
42
43
|
// back to the internal base so commands remain functional in local-only mode.
|
|
43
44
|
const publicGatewayBase = getIngressPublicBaseUrl()?.replace(/\/+$/, '');
|
|
44
45
|
env.GATEWAY_BASE_URL = publicGatewayBase || internalGatewayBase;
|
|
46
|
+
// Mint a short-lived JWT for gateway authentication when the signing key
|
|
47
|
+
// is available (daemon context). CLI-only contexts without daemon startup
|
|
48
|
+
// will not have the key initialized — gracefully skip.
|
|
49
|
+
if (isSigningKeyInitialized()) {
|
|
50
|
+
env.GATEWAY_AUTH_TOKEN = mintEdgeRelayToken();
|
|
51
|
+
}
|
|
45
52
|
return env;
|
|
46
53
|
}
|