@vellumai/assistant 0.4.18 → 0.4.20

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.
Files changed (36) hide show
  1. package/docs/runbook-trusted-contacts.md +5 -3
  2. package/package.json +1 -1
  3. package/src/__tests__/channel-approvals.test.ts +7 -1
  4. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  5. package/src/__tests__/daemon-server-session-init.test.ts +2 -0
  6. package/src/__tests__/gmail-integration.test.ts +13 -4
  7. package/src/__tests__/handle-user-message-secret-resume.test.ts +7 -1
  8. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -0
  9. package/src/__tests__/ingress-reconcile.test.ts +13 -5
  10. package/src/__tests__/mcp-cli.test.ts +1 -1
  11. package/src/__tests__/recording-intent-handler.test.ts +9 -1
  12. package/src/__tests__/send-endpoint-busy.test.ts +8 -2
  13. package/src/__tests__/sms-messaging-provider.test.ts +4 -0
  14. package/src/__tests__/system-prompt.test.ts +18 -2
  15. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  16. package/src/agent/loop.ts +324 -163
  17. package/src/cli/mcp.ts +81 -28
  18. package/src/config/bundled-skills/app-builder/SKILL.md +7 -5
  19. package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
  20. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +6 -11
  21. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -2
  22. package/src/config/bundled-skills/sms-setup/SKILL.md +8 -16
  23. package/src/config/bundled-skills/telegram-setup/SKILL.md +3 -3
  24. package/src/config/bundled-skills/trusted-contacts/SKILL.md +13 -25
  25. package/src/config/bundled-skills/twilio-setup/SKILL.md +13 -23
  26. package/src/config/system-prompt.ts +574 -518
  27. package/src/daemon/session-surfaces.ts +28 -0
  28. package/src/daemon/session.ts +255 -191
  29. package/src/daemon/tool-side-effects.ts +3 -13
  30. package/src/mcp/client.ts +2 -7
  31. package/src/security/secure-keys.ts +43 -3
  32. package/src/tools/apps/definitions.ts +5 -0
  33. package/src/tools/apps/executors.ts +18 -22
  34. package/src/tools/terminal/safe-env.ts +7 -0
  35. package/src/__tests__/response-tier.test.ts +0 -195
  36. package/src/daemon/response-tier.ts +0 -250
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -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(3);
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
 
@@ -131,6 +131,8 @@ class MockSession {
131
131
  this.guardianContext = ctx;
132
132
  }
133
133
 
134
+ setAuthContext(): void {}
135
+
134
136
  setChannelCapabilities(): void {}
135
137
 
136
138
  setCommandIntent(): void {}
@@ -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 manifestToolNames: string[] = toolsManifest.tools.map(
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(manifestToolNames).toContain(name);
84
+ expect(slackToolNames).toContain(name);
78
85
  }
79
86
  });
80
87
 
81
- test("TOOLS.json manifest contains at least the expected number of tools", () => {
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
- expect(toolsManifest.tools.length).toBeGreaterThanOrEqual(expectedMinimum);
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
- mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
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("skips reconcile trigger when no HTTP bearer token is available", async () => {
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
- // No reconcile call should have been made
210
- expect(reconcileCalls).toHaveLength(0);
217
+ // Reconcile is always triggered using mintDaemonDeliveryToken
218
+ expect(reconcileCalls).toHaveLength(1);
211
219
  });
212
220
 
213
- test("triggers reconcile when HTTP bearer token is available", async () => {
214
- httpTokenValue = "test-bearer-token";
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
- mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
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
- setGuardianContext: () => {},
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: "guardian-vellum",
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,8 +225,23 @@ describe("buildSystemPrompt", () => {
224
225
  expect(section).toContain("Occasional variations are acceptable");
225
226
  });
226
227
 
227
- test("includes memory persistence section in high tier", () => {
228
- const result = buildSystemPrompt("high");
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("includes memory persistence section", () => {
244
+ const result = buildSystemPrompt();
229
245
  expect(result).toContain("## Memory Persistence");
230
246
  expect(result).toContain("memory_save");
231
247
  expect(result).toContain("Saved > unsaved. Always.");
@@ -78,6 +78,7 @@ 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",
82
83
  getSocketPath: () => "/tmp/vellum.sock",
83
84
  }));