@vellumai/assistant 0.4.23 → 0.4.26

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 (63) hide show
  1. package/bun.lock +3 -0
  2. package/package.json +2 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -15
  4. package/src/__tests__/assistant-events-sse-hardening.test.ts +9 -3
  5. package/src/__tests__/call-controller.test.ts +80 -0
  6. package/src/__tests__/config-schema.test.ts +38 -178
  7. package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
  8. package/src/__tests__/credential-security-invariants.test.ts +0 -2
  9. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
  10. package/src/__tests__/ipc-snapshot.test.ts +0 -9
  11. package/src/__tests__/onboarding-template-contract.test.ts +10 -20
  12. package/src/__tests__/relay-server.test.ts +3 -3
  13. package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
  14. package/src/__tests__/runtime-events-sse.test.ts +7 -0
  15. package/src/__tests__/session-runtime-assembly.test.ts +34 -8
  16. package/src/__tests__/system-prompt.test.ts +7 -1
  17. package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
  18. package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
  19. package/src/__tests__/twilio-routes.test.ts +2 -3
  20. package/src/__tests__/voice-quality.test.ts +21 -132
  21. package/src/calls/call-controller.ts +34 -29
  22. package/src/calls/relay-server.ts +11 -5
  23. package/src/calls/twilio-routes.ts +4 -38
  24. package/src/calls/voice-quality.ts +7 -63
  25. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
  26. package/src/config/bundled-skills/messaging/SKILL.md +3 -5
  27. package/src/config/bundled-skills/phone-calls/SKILL.md +144 -83
  28. package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
  29. package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
  30. package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
  31. package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
  32. package/src/config/calls-schema.ts +3 -53
  33. package/src/config/elevenlabs-schema.ts +33 -0
  34. package/src/config/schema.ts +183 -137
  35. package/src/config/types.ts +0 -1
  36. package/src/daemon/handlers/browser.ts +1 -6
  37. package/src/daemon/ipc-contract/browser.ts +5 -14
  38. package/src/daemon/ipc-contract-inventory.json +0 -2
  39. package/src/daemon/session-agent-loop-handlers.ts +3 -0
  40. package/src/daemon/session-runtime-assembly.ts +9 -7
  41. package/src/mcp/client.ts +2 -1
  42. package/src/memory/conversation-crud.ts +339 -166
  43. package/src/runtime/auth/middleware.ts +87 -26
  44. package/src/runtime/routes/events-routes.ts +7 -0
  45. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  46. package/src/schedule/scheduler.ts +159 -45
  47. package/src/security/secure-keys.ts +3 -3
  48. package/src/tools/browser/browser-manager.ts +72 -228
  49. package/src/tools/browser/browser-screencast.ts +0 -5
  50. package/src/tools/network/script-proxy/certs.ts +7 -237
  51. package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
  52. package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
  53. package/src/tools/network/script-proxy/logging.ts +12 -196
  54. package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
  55. package/src/tools/network/script-proxy/policy.ts +4 -152
  56. package/src/tools/network/script-proxy/router.ts +2 -60
  57. package/src/tools/network/script-proxy/server.ts +5 -137
  58. package/src/tools/network/script-proxy/types.ts +19 -125
  59. package/src/tools/system/voice-config.ts +23 -1
  60. package/src/util/logger.ts +4 -1
  61. package/src/__tests__/elevenlabs-config.test.ts +0 -95
  62. package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
  63. package/src/calls/elevenlabs-config.ts +0 -32
@@ -1,125 +1,19 @@
1
- /** Unique identifier for a proxy session. */
2
- export type ProxySessionId = string;
3
-
4
- export type ProxySessionStatus = 'starting' | 'active' | 'stopping' | 'stopped';
5
-
6
- export interface ProxySession {
7
- id: ProxySessionId;
8
- conversationId: string;
9
- credentialIds: string[];
10
- status: ProxySessionStatus;
11
- createdAt: Date;
12
- /** Ephemeral port assigned once the session starts listening. */
13
- port: number | null;
14
- }
15
-
16
- export interface ProxySessionConfig {
17
- /** How long (ms) an idle session stays alive before auto-stopping. */
18
- idleTimeoutMs: number;
19
- /** Maximum concurrent sessions per conversation. */
20
- maxSessionsPerConversation: number;
21
- }
22
-
23
- export interface ProxyEnvVars {
24
- HTTP_PROXY: string;
25
- HTTPS_PROXY: string;
26
- NO_PROXY: string;
27
- NODE_EXTRA_CA_CERTS?: string;
28
- /** Combined CA bundle (system roots + proxy CA) for non-Node TLS clients (curl, Python, etc.). */
29
- SSL_CERT_FILE?: string;
30
- }
31
-
32
- // ---------------------------------------------------------------------------
33
- // Policy engine types
34
- // ---------------------------------------------------------------------------
35
-
36
- import type { CredentialInjectionTemplate } from '../../credentials/policy-types.js';
37
-
38
- /** A single credential matched — inject it. */
39
- export interface PolicyDecisionMatched {
40
- kind: 'matched';
41
- credentialId: string;
42
- template: CredentialInjectionTemplate;
43
- }
44
-
45
- /** Multiple credentials match — caller must disambiguate. */
46
- export interface PolicyDecisionAmbiguous {
47
- kind: 'ambiguous';
48
- candidates: Array<{ credentialId: string; template: CredentialInjectionTemplate }>;
49
- }
50
-
51
- /** No credential matches the target host/path. */
52
- export interface PolicyDecisionMissing {
53
- kind: 'missing';
54
- }
55
-
56
- /** No credential_ids were requested — pass-through. */
57
- export interface PolicyDecisionUnauthenticated {
58
- kind: 'unauthenticated';
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // Approval hook outcomes — structured data for triggering permission prompts.
63
- // ---------------------------------------------------------------------------
64
-
65
- /** Context about the outbound request target, used to build permission prompts. */
66
- export interface RequestTargetContext {
67
- hostname: string;
68
- port: number | null;
69
- path: string;
70
- /** The protocol scheme of the original request ('http' or 'https'). */
71
- scheme: 'http' | 'https';
72
- }
73
-
74
- /**
75
- * The target host matches a known credential template pattern, but the
76
- * session has no credential bound for it. The UI should prompt the user
77
- * to bind or create a credential.
78
- */
79
- export interface PolicyDecisionAskMissingCredential {
80
- kind: 'ask_missing_credential';
81
- target: RequestTargetContext;
82
- /** Host patterns from the known registry that matched the target. */
83
- matchingPatterns: string[];
84
- }
85
-
86
- /**
87
- * The request doesn't match any known credential template and the session
88
- * has no credentials. The UI should prompt the user to allow or deny the
89
- * unauthenticated request.
90
- */
91
- export interface PolicyDecisionAskUnauthenticated {
92
- kind: 'ask_unauthenticated';
93
- target: RequestTargetContext;
94
- }
95
-
96
- export type PolicyDecision =
97
- | PolicyDecisionMatched
98
- | PolicyDecisionAmbiguous
99
- | PolicyDecisionMissing
100
- | PolicyDecisionUnauthenticated
101
- | PolicyDecisionAskMissingCredential
102
- | PolicyDecisionAskUnauthenticated;
103
-
104
- // ---------------------------------------------------------------------------
105
- // Proxy approval callback — wires policy "ask" decisions to the UI prompter.
106
- // ---------------------------------------------------------------------------
107
-
108
- /**
109
- * Payload passed to the approval callback when the policy engine emits an
110
- * `ask_missing_credential` or `ask_unauthenticated` decision. Contains
111
- * enough context for the prompter to build a meaningful confirmation dialog.
112
- */
113
- export interface ProxyApprovalRequest {
114
- /** The policy decision that triggered the approval prompt. */
115
- decision: PolicyDecisionAskMissingCredential | PolicyDecisionAskUnauthenticated;
116
- /** The proxy session ID that originated the request. */
117
- sessionId: ProxySessionId;
118
- }
119
-
120
- /**
121
- * Callback signature for proxy approval prompts. The proxy service calls
122
- * this when an outbound request requires user confirmation. Returns `true`
123
- * if the user approves, `false` if denied.
124
- */
125
- export type ProxyApprovalCallback = (request: ProxyApprovalRequest) => Promise<boolean>;
1
+ export type {
2
+ CredentialInjectionTemplate,
3
+ CredentialInjectionType,
4
+ PolicyDecision,
5
+ PolicyDecisionAmbiguous,
6
+ PolicyDecisionAskMissingCredential,
7
+ PolicyDecisionAskUnauthenticated,
8
+ PolicyDecisionMatched,
9
+ PolicyDecisionMissing,
10
+ PolicyDecisionUnauthenticated,
11
+ ProxyApprovalCallback,
12
+ ProxyApprovalRequest,
13
+ ProxyEnvVars,
14
+ ProxySession,
15
+ ProxySessionConfig,
16
+ ProxySessionId,
17
+ ProxySessionStatus,
18
+ RequestTargetContext,
19
+ } from "@vellumai/proxy-sidecar";
@@ -1,3 +1,4 @@
1
+ import { invalidateConfigCache, loadRawConfig, saveRawConfig, setNestedValue } from '../../config/loader.js';
1
2
  import { normalizeActivationKey } from '../../daemon/handlers/config-voice.js';
2
3
  import { RiskLevel } from '../../permissions/types.js';
3
4
  import type { ToolDefinition } from '../../providers/types.js';
@@ -11,6 +12,7 @@ const VOICE_SETTINGS = {
11
12
  wake_word_enabled: { userDefaultsKey: 'wakeWordEnabled', type: 'boolean' as const },
12
13
  wake_word_keyword: { userDefaultsKey: 'wakeWordKeyword', type: 'string' as const },
13
14
  wake_word_timeout: { userDefaultsKey: 'wakeWordTimeoutSeconds', type: 'number' as const },
15
+ tts_voice_id: { userDefaultsKey: 'ttsVoiceId', type: 'string' as const },
14
16
  } as const;
15
17
 
16
18
  type VoiceSettingName = keyof typeof VOICE_SETTINGS;
@@ -55,6 +57,16 @@ function validateSetting(setting: string, value: unknown): { ok: true; coerced:
55
57
  }
56
58
  return { ok: true, coerced: num };
57
59
  }
60
+ case 'tts_voice_id': {
61
+ if (typeof value !== 'string' || value.trim().length === 0) {
62
+ return { ok: false, error: 'tts_voice_id must be a non-empty string (ElevenLabs voice ID)' };
63
+ }
64
+ const trimmed = value.trim();
65
+ if (!/^[a-zA-Z0-9]+$/.test(trimmed)) {
66
+ return { ok: false, error: 'tts_voice_id must contain only alphanumeric characters (ElevenLabs voice ID format)' };
67
+ }
68
+ return { ok: true, coerced: trimmed };
69
+ }
58
70
  default:
59
71
  return { ok: false, error: `Unknown setting "${setting}"` };
60
72
  }
@@ -65,12 +77,13 @@ const FRIENDLY_NAMES: Record<VoiceSettingName, string> = {
65
77
  wake_word_enabled: 'Wake word',
66
78
  wake_word_keyword: 'Wake word keyword',
67
79
  wake_word_timeout: 'Wake word timeout',
80
+ tts_voice_id: 'ElevenLabs voice',
68
81
  };
69
82
 
70
83
  export class VoiceConfigUpdateTool implements Tool {
71
84
  name = 'voice_config_update';
72
85
  description =
73
- 'Update a voice configuration setting (PTT activation key, wake word enabled/keyword/timeout). ' +
86
+ 'Update a voice configuration setting (TTS voice ID, PTT activation key, wake word enabled/keyword/timeout). ' +
74
87
  'Changes take effect immediately via IPC broadcast to the desktop client.';
75
88
  category = 'system';
76
89
  defaultRiskLevel = RiskLevel.Low;
@@ -141,6 +154,15 @@ export class VoiceConfigUpdateTool implements Tool {
141
154
  });
142
155
  }
143
156
 
157
+ // For tts_voice_id, also persist to the config file (elevenlabs.voiceId)
158
+ // so phone calls and other consumers pick it up.
159
+ if (setting === 'tts_voice_id') {
160
+ const raw = loadRawConfig();
161
+ setNestedValue(raw, 'elevenlabs.voiceId', validation.coerced);
162
+ saveRawConfig(raw);
163
+ invalidateConfigCache();
164
+ }
165
+
144
166
  return {
145
167
  content: `${friendlyName} updated to ${JSON.stringify(validation.coerced)}. The change has been broadcast to the desktop client.`,
146
168
  isError: false,
@@ -156,7 +156,10 @@ function getRootLogger(): pino.Logger {
156
156
 
157
157
  try {
158
158
  const logPath = getLogPath();
159
- const fileDest = pino.destination({ dest: logPath, sync: false, mkdir: true, mode: 0o600 });
159
+ // Use sync: true so the fd is opened immediately. This prevents
160
+ // "sonic boom is not ready yet" errors when commander calls
161
+ // process.exit(0) for --help/--version before the async fd is ready.
162
+ const fileDest = pino.destination({ dest: logPath, sync: true, mkdir: true, mode: 0o600 });
160
163
  // Tighten permissions on pre-existing log files that may have been created with looser modes
161
164
  try { chmodSync(logPath, 0o600); } catch { /* best-effort */ }
162
165
  const fileStream = pinoPretty(prettyOpts({ destination: fileDest, colorize: false }));
@@ -1,95 +0,0 @@
1
- import { describe, expect, mock, test } from "bun:test";
2
-
3
- // ── Mocks (must come before source imports) ──────────────────────────
4
-
5
- let mockApiKey: string | null = null;
6
-
7
- mock.module("../util/logger.js", () => ({
8
- getLogger: () =>
9
- new Proxy({} as Record<string, unknown>, {
10
- get: () => () => {},
11
- }),
12
- }));
13
-
14
- mock.module("../security/secure-keys.js", () => ({
15
- getSecureKey: (key: string) => {
16
- if (key === "credential:elevenlabs:api_key") return mockApiKey;
17
- return null;
18
- },
19
- }));
20
-
21
- let mockVoiceConfig = {
22
- elevenlabs: {
23
- agentId: "agent-123",
24
- apiBaseUrl: "https://api.elevenlabs.io",
25
- registerCallTimeoutMs: 5000,
26
- },
27
- };
28
-
29
- mock.module("../config/loader.js", () => ({
30
- getConfig: () => ({
31
- ui: {},
32
-
33
- calls: { voice: mockVoiceConfig },
34
- }),
35
- }));
36
-
37
- import { getElevenLabsConfig } from "../calls/elevenlabs-config.js";
38
-
39
- describe("elevenlabs-config", () => {
40
- test("returns config when API key and agent ID are set", () => {
41
- mockApiKey = "sk-test-key-123";
42
- mockVoiceConfig = {
43
- elevenlabs: {
44
- agentId: "agent-abc",
45
- apiBaseUrl: "https://api.elevenlabs.io",
46
- registerCallTimeoutMs: 10000,
47
- },
48
- };
49
-
50
- const config = getElevenLabsConfig();
51
- expect(config.apiKey).toBe("sk-test-key-123");
52
- expect(config.agentId).toBe("agent-abc");
53
- expect(config.apiBaseUrl).toBe("https://api.elevenlabs.io");
54
- expect(config.registerCallTimeoutMs).toBe(10000);
55
- });
56
-
57
- test("throws ConfigError when API key is missing", () => {
58
- mockApiKey = null;
59
- mockVoiceConfig = {
60
- elevenlabs: {
61
- agentId: "agent-abc",
62
- apiBaseUrl: "https://api.elevenlabs.io",
63
- registerCallTimeoutMs: 5000,
64
- },
65
- };
66
-
67
- expect(() => getElevenLabsConfig()).toThrow(/API key is not configured/);
68
- });
69
-
70
- test("throws ConfigError when API key is empty string", () => {
71
- mockApiKey = "";
72
- mockVoiceConfig = {
73
- elevenlabs: {
74
- agentId: "agent-abc",
75
- apiBaseUrl: "https://api.elevenlabs.io",
76
- registerCallTimeoutMs: 5000,
77
- },
78
- };
79
-
80
- expect(() => getElevenLabsConfig()).toThrow(/API key is not configured/);
81
- });
82
-
83
- test("throws ConfigError when agent ID is missing", () => {
84
- mockApiKey = "sk-valid";
85
- mockVoiceConfig = {
86
- elevenlabs: {
87
- agentId: "",
88
- apiBaseUrl: "https://api.elevenlabs.io",
89
- registerCallTimeoutMs: 5000,
90
- },
91
- };
92
-
93
- expect(() => getElevenLabsConfig()).toThrow(/agent ID is not configured/);
94
- });
95
- });