@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,82 +1 @@
1
- /**
2
- * CONNECT tunnel handler — establishes a raw TCP tunnel between the
3
- * client and a remote host for HTTPS pass-through (no MITM).
4
- */
5
-
6
- import type { IncomingMessage } from 'node:http';
7
- import { connect, type Socket } from 'node:net';
8
-
9
- /**
10
- * Parse and validate a CONNECT target of the form `host:port`.
11
- * Returns null if the target is malformed.
12
- * Strips brackets from IPv6 literals (e.g. `[::1]:443` -> `::1`).
13
- */
14
- function parseTarget(url: string | undefined): { host: string; port: number } | null {
15
- if (!url) return null;
16
-
17
- const colonIdx = url.lastIndexOf(':');
18
- if (colonIdx <= 0) return null; // no port separator, or leading colon only
19
-
20
- let host = url.slice(0, colonIdx);
21
- const portStr = url.slice(colonIdx + 1);
22
-
23
- if (!host || !portStr) return null;
24
-
25
- const port = Number(portStr);
26
- if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
27
-
28
- // Strip brackets from IPv6 literals — net.connect expects the raw address
29
- if (host.startsWith('[') && host.endsWith(']')) {
30
- host = host.slice(1, -1);
31
- if (!host) return null;
32
- }
33
-
34
- return { host, port };
35
- }
36
-
37
- /**
38
- * Handle an HTTP CONNECT request by establishing a bidirectional TCP
39
- * tunnel to the requested target.
40
- */
41
- export function handleConnect(
42
- req: IncomingMessage,
43
- clientSocket: Socket,
44
- head: Buffer,
45
- ): void {
46
- const target = parseTarget(req.url);
47
-
48
- if (!target) {
49
- clientSocket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
50
- clientSocket.destroy();
51
- return;
52
- }
53
-
54
- let tunnelEstablished = false;
55
-
56
- const upstream = connect(target.port, target.host, () => {
57
- tunnelEstablished = true;
58
- clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
59
-
60
- // Forward any data already buffered by the HTTP parser
61
- if (head.length > 0) {
62
- upstream.write(head);
63
- }
64
-
65
- // Bidirectional piping
66
- upstream.pipe(clientSocket);
67
- clientSocket.pipe(upstream);
68
- });
69
-
70
- upstream.on('error', () => {
71
- // Only send HTTP error if the tunnel hasn't been established yet;
72
- // once established the client expects raw TLS, not HTTP framing
73
- if (!tunnelEstablished && clientSocket.writable) {
74
- clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
75
- }
76
- clientSocket.destroy();
77
- });
78
-
79
- clientSocket.on('error', () => {
80
- upstream.destroy();
81
- });
82
- }
1
+ export { handleConnect } from "@vellumai/proxy-sidecar";
@@ -1,151 +1,2 @@
1
- /**
2
- * HTTP proxy forwarder — parses absolute-URL proxy requests and forwards
3
- * them to the upstream server with full body streaming.
4
- */
5
-
6
- import { type IncomingMessage, request as httpRequest, type ServerResponse } from 'node:http';
7
- import { URL } from 'node:url';
8
-
9
- /** Hop-by-hop headers that MUST NOT be forwarded between proxy hops. */
10
- const HOP_BY_HOP = new Set([
11
- 'connection',
12
- 'keep-alive',
13
- 'proxy-authenticate',
14
- 'proxy-authorization',
15
- 'proxy-connection',
16
- 'te',
17
- 'trailer',
18
- 'transfer-encoding',
19
- 'upgrade',
20
- ]);
21
-
22
- /**
23
- * Optional callback for credential injection or policy gating.
24
- * Called before the upstream request is sent. Returns extra headers
25
- * to merge, or null to reject the request.
26
- */
27
- export type PolicyCallback = (
28
- hostname: string,
29
- port: number | null,
30
- path: string,
31
- scheme: 'http' | 'https',
32
- ) => Promise<Record<string, string> | null>;
33
-
34
- /**
35
- * Strip hop-by-hop headers and Connection-token headers (RFC 7230 s6.1)
36
- * from an incoming header set, preserving multi-value arrays.
37
- */
38
- function filterHeaders(raw: IncomingMessage['headers']): Record<string, string | string[]> {
39
- // Collect extra headers listed in the Connection header (RFC 7230 s6.1)
40
- const connectionTokens = new Set<string>();
41
- const connValue = raw['connection'];
42
- if (connValue) {
43
- const values = Array.isArray(connValue) ? connValue : [connValue];
44
- for (const v of values) {
45
- for (const token of v.split(',')) {
46
- connectionTokens.add(token.trim().toLowerCase());
47
- }
48
- }
49
- }
50
-
51
- const out: Record<string, string | string[]> = {};
52
- for (const [key, value] of Object.entries(raw)) {
53
- const lower = key.toLowerCase();
54
- if (HOP_BY_HOP.has(lower)) continue;
55
- if (connectionTokens.has(lower)) continue;
56
- if (value === undefined) continue;
57
- out[key] = value;
58
- }
59
- return out;
60
- }
61
-
62
- /**
63
- * Forward a plain HTTP proxy request (absolute-URL form) to the upstream
64
- * server and stream the response back to the client.
65
- */
66
- export function forwardHttpRequest(
67
- clientReq: IncomingMessage,
68
- clientRes: ServerResponse,
69
- policyCallback?: PolicyCallback,
70
- ): void {
71
- const urlStr = clientReq.url;
72
- if (!urlStr) {
73
- clientRes.writeHead(400, { 'Content-Type': 'text/plain' });
74
- clientRes.end('Bad Request');
75
- return;
76
- }
77
-
78
- let parsed: URL;
79
- try {
80
- parsed = new URL(urlStr);
81
- } catch {
82
- clientRes.writeHead(400, { 'Content-Type': 'text/plain' });
83
- clientRes.end('Bad Request');
84
- return;
85
- }
86
-
87
- if (parsed.protocol !== 'http:') {
88
- clientRes.writeHead(400, { 'Content-Type': 'text/plain' });
89
- clientRes.end('Only HTTP is supported for non-CONNECT proxy requests');
90
- return;
91
- }
92
-
93
- const hostname = parsed.hostname;
94
- const port = parsed.port ? Number(parsed.port) : 80;
95
- const path = parsed.pathname + parsed.search;
96
-
97
- const doForward = (extraHeaders: Record<string, string> = {}) => {
98
- const headers = { ...filterHeaders(clientReq.headers), ...extraHeaders };
99
- // Ensure Host header matches the upstream target
100
- headers['host'] = parsed.host;
101
-
102
- const upstreamReq = httpRequest(
103
- {
104
- hostname,
105
- port,
106
- path,
107
- method: clientReq.method,
108
- headers,
109
- },
110
- (upstreamRes: IncomingMessage) => {
111
- const responseHeaders = filterHeaders(upstreamRes.headers);
112
- clientRes.writeHead(upstreamRes.statusCode ?? 502, responseHeaders);
113
- upstreamRes.on('error', () => {
114
- clientRes.destroy();
115
- });
116
- upstreamRes.pipe(clientRes);
117
- },
118
- );
119
-
120
- upstreamReq.on('error', () => {
121
- // Don't leak internal error details — generic 502
122
- if (!clientRes.headersSent) {
123
- clientRes.writeHead(502, { 'Content-Type': 'text/plain' });
124
- }
125
- clientRes.end('Bad Gateway');
126
- });
127
-
128
- // Stream client body to upstream
129
- clientReq.pipe(upstreamReq);
130
- };
131
-
132
- if (policyCallback) {
133
- policyCallback(hostname, parsed.port ? Number(parsed.port) : null, path, 'http')
134
- .then((extraHeaders) => {
135
- if (extraHeaders == null) {
136
- clientRes.writeHead(403, { 'Content-Type': 'text/plain' });
137
- clientRes.end('Forbidden');
138
- return;
139
- }
140
- doForward(extraHeaders);
141
- })
142
- .catch(() => {
143
- if (!clientRes.headersSent) {
144
- clientRes.writeHead(502, { 'Content-Type': 'text/plain' });
145
- }
146
- clientRes.end('Bad Gateway');
147
- });
148
- } else {
149
- doForward();
150
- }
151
- }
1
+ export type { PolicyCallback } from "@vellumai/proxy-sidecar";
2
+ export { forwardHttpRequest } from "@vellumai/proxy-sidecar";
@@ -1,196 +1,12 @@
1
- /**
2
- * Safe diagnostic logging helpers for the proxy subsystem.
3
- *
4
- * All sanitizers and trace builders are designed to NEVER include secret
5
- * values by construction — sanitizers redact sensitive header/query values,
6
- * and trace builders only reference host patterns, decision kinds, and
7
- * candidate counts.
8
- */
9
-
10
- const REDACTED = '[REDACTED]';
11
-
12
- /**
13
- * Replace values of sensitive header keys with a redaction placeholder.
14
- *
15
- * Matching is case-insensitive — "Authorization" and "authorization"
16
- * are both caught. The caller supplies the set of sensitive key names
17
- * (lowercased) because different credential templates inject into
18
- * different headers.
19
- */
20
- export function sanitizeHeaders(
21
- headers: Record<string, string>,
22
- sensitiveKeys: string[],
23
- ): Record<string, string> {
24
- const lower = new Set(sensitiveKeys.map((k) => k.toLowerCase()));
25
- const out: Record<string, string> = {};
26
-
27
- for (const [key, value] of Object.entries(headers)) {
28
- out[key] = lower.has(key.toLowerCase()) ? REDACTED : value;
29
- }
30
-
31
- return out;
32
- }
33
-
34
- /**
35
- * Redact query-parameter values for sensitive param names.
36
- *
37
- * Returns a URL string where the values of `sensitiveParams` are
38
- * replaced with the redaction placeholder. Non-sensitive params and
39
- * the rest of the URL are preserved verbatim.
40
- */
41
- export function sanitizeUrl(
42
- url: string,
43
- sensitiveParams: string[],
44
- ): string {
45
- if (sensitiveParams.length === 0) return url;
46
-
47
- // Guard against malformed input — return the URL unchanged if it
48
- // doesn't contain a query string at all.
49
- const qIdx = url.indexOf('?');
50
- if (qIdx === -1) return url;
51
-
52
- try {
53
- // Build a full URL if given an absolute path, otherwise parse as-is
54
- const parseable = url.startsWith('/') ? `http://placeholder${url}` : url;
55
- const parsed = new URL(parseable);
56
- const lower = new Set(sensitiveParams.map((p) => p.toLowerCase()));
57
-
58
- for (const key of Array.from(parsed.searchParams.keys())) {
59
- if (lower.has(key.toLowerCase())) {
60
- parsed.searchParams.set(key, REDACTED);
61
- }
62
- }
63
-
64
- // Reconstruct the original shape: if the input was a path we strip
65
- // the placeholder origin so the caller gets back a relative path.
66
- if (url.startsWith('/')) {
67
- return parsed.pathname + parsed.search;
68
- }
69
- return parsed.toString();
70
- } catch {
71
- // Fail closed: if we can't parse the URL, strip the query string
72
- // entirely rather than risk leaking secrets in log output.
73
- return url.slice(0, qIdx);
74
- }
75
- }
76
-
77
- /**
78
- * Build a log-safe snapshot of an outbound proxy request.
79
- *
80
- * `sensitiveKeys` should include header names and query param names
81
- * that carry credential values (e.g. "Authorization", "api_key").
82
- */
83
- export function createSafeLogEntry(
84
- req: { method: string; url: string; headers: Record<string, string> },
85
- sensitiveKeys: string[],
86
- ): { method: string; url: string; headers: Record<string, string> } {
87
- return {
88
- method: req.method,
89
- url: sanitizeUrl(req.url, sensitiveKeys),
90
- headers: sanitizeHeaders(req.headers, sensitiveKeys),
91
- };
92
- }
93
-
94
- // ---------------------------------------------------------------------------
95
- // Policy/rewrite decision trace
96
- // ---------------------------------------------------------------------------
97
-
98
- import type { PolicyDecision } from './types.js';
99
-
100
- export interface ProxyDecisionTrace {
101
- /** Target hostname. */
102
- host: string;
103
- /** Target port (null = default for scheme). */
104
- port: number | null;
105
- /** Request path. */
106
- path: string;
107
- /** Protocol scheme. */
108
- scheme: 'http' | 'https';
109
- /** The decision kind emitted by the policy engine. */
110
- decisionKind: PolicyDecision['kind'];
111
- /** Number of candidate templates that matched before disambiguation. */
112
- candidateCount: number;
113
- /** The host pattern of the selected template, if any. */
114
- selectedPattern: string | null;
115
- /** The credential ID of the selected credential, if any. */
116
- selectedCredentialId: string | null;
117
- }
118
-
119
- /**
120
- * Strip the query string from a URL path so that secrets passed as
121
- * query parameters (API keys, tokens) are never recorded in traces.
122
- */
123
- export function stripQueryString(p: string): string {
124
- const idx = p.indexOf('?');
125
- return idx === -1 ? p : p.slice(0, idx);
126
- }
127
-
128
- /**
129
- * Build a structured trace record from a policy decision.
130
- *
131
- * Intentionally excludes all secret-bearing fields (header values,
132
- * storage keys, injected tokens) — only patterns, counts, and
133
- * decision metadata are included. Query parameters are stripped from
134
- * the path to prevent leaking secrets (API keys, tokens) into logs.
135
- */
136
- export function buildDecisionTrace(
137
- host: string,
138
- port: number | null,
139
- path: string,
140
- scheme: 'http' | 'https',
141
- decision: PolicyDecision,
142
- ): ProxyDecisionTrace {
143
- let candidateCount = 0;
144
- let selectedPattern: string | null = null;
145
- let selectedCredentialId: string | null = null;
146
-
147
- switch (decision.kind) {
148
- case 'matched':
149
- candidateCount = 1;
150
- selectedPattern = decision.template.hostPattern;
151
- selectedCredentialId = decision.credentialId;
152
- break;
153
- case 'ambiguous':
154
- candidateCount = decision.candidates.length;
155
- break;
156
- case 'ask_missing_credential':
157
- candidateCount = decision.matchingPatterns.length;
158
- break;
159
- // 'missing', 'unauthenticated', 'ask_unauthenticated' — no candidates
160
- }
161
-
162
- return {
163
- host,
164
- port,
165
- path: stripQueryString(path),
166
- scheme,
167
- decisionKind: decision.kind,
168
- candidateCount,
169
- selectedPattern,
170
- selectedCredentialId,
171
- };
172
- }
173
-
174
- // ---------------------------------------------------------------------------
175
- // Credential ref resolution trace
176
- // ---------------------------------------------------------------------------
177
-
178
- export interface CredentialRefTrace {
179
- /** The raw refs provided by the caller. */
180
- rawRefs: string[];
181
- /** The resolved canonical UUIDs. */
182
- resolvedIds: string[];
183
- /** Any refs that could not be resolved. */
184
- unresolvedRefs: string[];
185
- }
186
-
187
- /**
188
- * Build a credential ref resolution trace for diagnostic logging.
189
- */
190
- export function buildCredentialRefTrace(
191
- rawRefs: string[],
192
- resolvedIds: string[],
193
- unresolvedRefs: string[],
194
- ): CredentialRefTrace {
195
- return { rawRefs, resolvedIds, unresolvedRefs };
196
- }
1
+ export type {
2
+ CredentialRefTrace,
3
+ ProxyDecisionTrace,
4
+ } from "@vellumai/proxy-sidecar";
5
+ export {
6
+ buildCredentialRefTrace,
7
+ buildDecisionTrace,
8
+ createSafeLogEntry,
9
+ sanitizeHeaders,
10
+ sanitizeUrl,
11
+ stripQueryString,
12
+ } from "@vellumai/proxy-sidecar";