@voicelayer/sdk 0.4.2 → 0.5.1

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/README.md CHANGED
@@ -333,6 +333,33 @@ node agent.ts start
333
333
 
334
334
  Agent names starting `voicelayer-` (and the names of VoiceLayer's own workers) are reserved; registering one is refused with `422 agent_name_reserved`.
335
335
 
336
+ ### Outbound HTTP from your agent
337
+
338
+ `httpTool`, a flow's tool steps and schema tools, and a process's completion `backendAck` all go through the SDK's
339
+ outbound guard. They use http(s) only, and any private, loopback, link-local or cloud-metadata address is refused, both
340
+ after DNS and on every redirect (at most 5 hops). The connection goes only to the address that was checked, a request
341
+ times out (a `backendAck` waits at most 10 s), and responses are capped at 2 MiB.
342
+
343
+ If your self-hosted worker has to call a service on your own network, opt in per tool or per ACK:
344
+
345
+ ```ts
346
+ httpTool({ name: 'inventory', description: '…', input: { sku: 'string' }, url: 'http://inventory.internal/{sku}', allowPrivateNetwork: true });
347
+ defineProcess({ collect: { … }, backendAck: { url: 'http://localhost:4000/ack', allowPrivateNetwork: true } });
348
+ ```
349
+
350
+ `allowPrivateNetwork` only lifts the private-address refusal. The request is still http(s), still pinned to the checked
351
+ address and still never follows a redirect blindly.
352
+
353
+ On `httpTool` you set it in your own code, so it takes effect directly. On a `backendAck`, or on a tool in a stored
354
+ flow or process schema, it takes effect only when the worker's operator also sets **`VL_ALLOW_PRIVATE_NETWORK=1`** in
355
+ the worker's environment, because an author's flag alone shouldn't open your network. VoiceLayer-hosted workers (those
356
+ holding `INTERNAL_SERVICE_TOKEN`) and the VoiceLayer API's text engine never honour it, whatever the environment says.
357
+ Their tool steps run through the platform's server-side guard, which has no such switch.
358
+
359
+ ```bash
360
+ export VL_ALLOW_PRIVATE_NETWORK=1 # self-hosted only: honour authors' allowPrivateNetwork on schema tools / backendAck
361
+ ```
362
+
336
363
  ### Texts
337
364
 
338
365
  `.start()` also registers a second LiveKit name, `<name>::text`, beside the voice one. A text message dispatched to it
@@ -1,3 +1,5 @@
1
+ import { O as OnQuery, B as BrainTransport, d as BrainMessage } from '../types-KqrAfY85.js';
2
+ export { a as BrainCallMetadata, b as BrainCapabilities, c as BrainChunk, e as BrainRequest, f as OnQueryContext, g as OnQueryResult } from '../types-KqrAfY85.js';
1
3
  import { Redis } from 'ioredis';
2
4
  import { z } from 'zod';
3
5
 
@@ -91,52 +93,6 @@ declare const ConnectorUpFrame: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
91
93
  }>]>;
92
94
  type ConnectorUpFrame = z.infer<typeof ConnectorUpFrame>;
93
95
 
94
- interface BrainMessage {
95
- readonly role: 'system' | 'user' | 'assistant';
96
- readonly content: string;
97
- }
98
- interface BrainCallMetadata {
99
- readonly channel: 'voice' | 'text';
100
- readonly callId?: string;
101
- readonly projectId?: string;
102
- }
103
- interface BrainRequest {
104
- readonly messages: readonly BrainMessage[];
105
- /** Upstream model id / alias. Omitted → the brain's own default. */
106
- readonly model?: string;
107
- readonly temperature?: number;
108
- readonly metadata: BrainCallMetadata;
109
- }
110
- /** One streamed piece of the brain's reply. */
111
- interface BrainChunk {
112
- readonly content?: string;
113
- }
114
- interface BrainCapabilities {
115
- readonly reachable: boolean;
116
- readonly streaming: boolean;
117
- }
118
- interface BrainTransport {
119
- readonly kind: 'callback' | 'http' | 'tunnel';
120
- /** Stream the reply token-by-token (the voice path: first sentence → TTS ASAP). */
121
- stream(req: BrainRequest, signal: AbortSignal): AsyncIterable<BrainChunk>;
122
- /** Non-streaming convenience (the text path). */
123
- complete(req: BrainRequest, signal: AbortSignal): Promise<string>;
124
- }
125
- /** Context handed to a customer's in-process `onQuery` brain. */
126
- interface OnQueryContext {
127
- /** Full conversation so far (system + prior turns + latest user message). */
128
- readonly messages: readonly BrainMessage[];
129
- /** Aborts when the caller barges in / the turn is cancelled. */
130
- readonly signal: AbortSignal;
131
- }
132
- type OnQueryResult = string | AsyncIterable<string>;
133
- /**
134
- * Bring-your-own brain, in-process. Receives the latest user utterance (and the
135
- * full history via `ctx.messages`) and returns the reply — either a string or an
136
- * async-iterable of string pieces for token streaming.
137
- */
138
- type OnQuery = (text: string, ctx: OnQueryContext) => OnQueryResult | Promise<OnQueryResult>;
139
-
140
96
  declare function callbackTransport(onQuery: OnQuery): BrainTransport;
141
97
 
142
98
  /** Convert a LK ChatContext (or any `{items}` shape) into BrainMessages. */
@@ -280,4 +236,4 @@ interface IncomingBrainRequest {
280
236
  /** Run one brain.request and yield the reply frames. Never throws. */
281
237
  declare function runBrainRequest(req: IncomingBrainRequest, cfg: BrainEndpointConfig, signal: AbortSignal): AsyncGenerator<ConnectorUpFrame>;
282
238
 
283
- export { type AssertUrlOptions, type BrainCallMetadata, type BrainCapabilities, type BrainChunk, BrainConfigError, type BrainEndpointConfig, type BrainMessage, type BrainPubSub, type BrainRequest, BrainRequestError, type BrainTransport, type ConnectorChatChunk, ConnectorChatModel, type ConnectorChatModelOptions, type ConnectorLLMOptions, type HostLookup, type HttpBrainTransportOptions, type IncomingBrainRequest, type OnQuery, type OnQueryContext, type OnQueryResult, RedisBrainPubSub, type SseDelta, type TunnelBrainTransportOptions, assertPublicHttpsUrl, buildRedisBrainPubSub, callbackTransport, chatChunkStream, chatContextToMessages, createConnectorLLM, httpBrainTransport, isDisallowedIp, lastUserText, parseChatCompletionSse, runBrainRequest, tunnelBrainTransport };
239
+ export { type AssertUrlOptions, BrainConfigError, type BrainEndpointConfig, BrainMessage, type BrainPubSub, BrainRequestError, BrainTransport, type ConnectorChatChunk, ConnectorChatModel, type ConnectorChatModelOptions, type ConnectorLLMOptions, type HostLookup, type HttpBrainTransportOptions, type IncomingBrainRequest, OnQuery, RedisBrainPubSub, type SseDelta, type TunnelBrainTransportOptions, assertPublicHttpsUrl, buildRedisBrainPubSub, callbackTransport, chatChunkStream, chatContextToMessages, createConnectorLLM, httpBrainTransport, isDisallowedIp, lastUserText, parseChatCompletionSse, runBrainRequest, tunnelBrainTransport };
@@ -434,6 +434,8 @@ var FlowNodeType = z.enum([
434
434
  "ask",
435
435
  "confirm",
436
436
  "tool",
437
+ // Send an email through the workspace's email (metered, wallet-gated; From on a verified domain) — burn-down G-22.
438
+ "email",
437
439
  "decision",
438
440
  "handoff",
439
441
  "end",
@@ -599,7 +601,10 @@ var ProcessFieldDTO = z.object({
599
601
  min: z.number().optional(),
600
602
  max: z.number().optional(),
601
603
  enum: z.array(z.string()).max(64).optional(),
602
- ask: z.string().max(512).optional()
604
+ ask: z.string().max(512).optional(),
605
+ /** false ⇒ written by the agent itself (a tool's output, an assignment), never said by the caller — the runtime
606
+ * doesn't extract it from their words. Absent ⇒ the caller may say it. */
607
+ fromCaller: z.boolean().optional()
603
608
  });
604
609
  var ProcessTriggerDTO = z.object({
605
610
  name: z.string().min(1).max(64),
@@ -618,7 +623,12 @@ var ProcessToolDTO = z.object({
618
623
  method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
619
624
  headers: z.record(z.string().max(1024)).optional(),
620
625
  auth: z.enum(["none", "connection"]),
621
- connectionRef: z.string().max(128).optional()
626
+ connectionRef: z.string().max(128).optional(),
627
+ /**
628
+ * Self-hosted workers only: allow a private / loopback address (a localhost service of your own). A platform
629
+ * worker (it holds the internal service token) never honours it.
630
+ */
631
+ allowPrivateNetwork: z.boolean().optional()
622
632
  });
623
633
  var ProcessSchemaDTO = z.object({
624
634
  id: z.string().min(1).max(64),
@@ -632,7 +642,9 @@ var ProcessSchemaDTO = z.object({
632
642
  requiredFields: z.array(z.string()).max(64),
633
643
  backendAck: z.object({
634
644
  url: z.string().url(),
635
- timeoutMs: z.number().int().positive()
645
+ timeoutMs: z.number().int().positive(),
646
+ // self-hosted workers only (see ProcessToolDTO.allowPrivateNetwork); a platform worker never honours it
647
+ allowPrivateNetwork: z.boolean().optional()
636
648
  }).optional()
637
649
  }),
638
650
  // Deterministic rails compiled from trigger/handoff/end nodes.
@@ -650,6 +662,9 @@ var ProcessSchemaDTO = z.object({
650
662
  }).optional(),
651
663
  // Tool capabilities compiled from tool nodes (maps to AgentConfig.tools).
652
664
  tools: z.array(ProcessToolDTO).max(32).optional(),
665
+ // The agent's human name, sealed at deploy (G-31): what a caller hears for {{ agent_name }} and what the dashboard
666
+ // shows. Distinct from the agent's dispatch name (a slug). Absent on schemas deployed before it.
667
+ displayName: z.string().min(1).max(128).optional(),
653
668
  // Voice turn-taking compiled from the flow's speech capability node (maps to
654
669
  // AgentConfig.speech → applySpeechTurnHandling). Absent = platform defaults.
655
670
  speech: z.object({
@@ -1382,6 +1397,56 @@ z.object({
1382
1397
  ok: z.literal(true),
1383
1398
  scopesPurged: z.array(MemoryScope)
1384
1399
  });
1400
+ var EMAIL_RE = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
1401
+ function isEmailAddress(value) {
1402
+ return EMAIL_RE.test(value) && value.length <= 254;
1403
+ }
1404
+ var EMAIL_SUBJECT_MAX = 200;
1405
+ var EMAIL_TEXT_MAX = 5e4;
1406
+ var EMAIL_HTML_MAX = 2e5;
1407
+ var address = z.string().max(254).refine(isEmailAddress, { message: "must be an email address" });
1408
+ z.object({
1409
+ to: address,
1410
+ subject: z.string().min(1).max(EMAIL_SUBJECT_MAX),
1411
+ text: z.string().min(1).max(EMAIL_TEXT_MAX),
1412
+ html: z.string().min(1).max(EMAIL_HTML_MAX).optional(),
1413
+ // An address on one of the workspace's verified sending domains; omitted → the platform sender.
1414
+ from: address.optional(),
1415
+ // The display name shown beside `from` (or beside the platform sender's address).
1416
+ fromName: z.string().trim().min(1).max(64).refine((v) => !/[\r\n<>"]/.test(v), { message: 'must not contain line breaks, <, > or "' }).optional(),
1417
+ // Where a reply goes — any address; it needs no verification.
1418
+ replyTo: address.optional(),
1419
+ // The conversation (a call, a text conversation) the email belongs to — a conversation sends at most 3 emails.
1420
+ conversationId: z.string().min(1).max(128).optional()
1421
+ });
1422
+ var DOMAIN_RE = /^(?=.{4,253}$)(?!-)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
1423
+ function normalizeDomain(input) {
1424
+ return input.trim().toLowerCase().replace(/^[a-z]+:\/\//, "").replace(/\/.*$/, "").replace(/\.$/, "");
1425
+ }
1426
+ function isDomainName(value) {
1427
+ return DOMAIN_RE.test(value);
1428
+ }
1429
+ z.object({
1430
+ domain: z.string().max(253).transform(normalizeDomain).refine(isDomainName, { message: "must be a domain like example.com" })
1431
+ });
1432
+ var EmailDomainStatus = z.enum(["pending", "verified", "failed"]);
1433
+ var EmailDnsRecord = z.object({
1434
+ type: z.enum(["CNAME", "TXT"]),
1435
+ name: z.string(),
1436
+ value: z.string(),
1437
+ // dkim: proves the domain (required); dmarc: recommended, only when the domain has no DMARC record yet.
1438
+ purpose: z.enum(["dkim", "dmarc"]),
1439
+ required: z.boolean()
1440
+ });
1441
+ z.object({
1442
+ id: z.string(),
1443
+ domain: z.string(),
1444
+ status: EmailDomainStatus,
1445
+ records: z.array(EmailDnsRecord),
1446
+ createdAt: z.string().datetime(),
1447
+ lastCheckedAt: z.string().datetime().nullable(),
1448
+ verifiedAt: z.string().datetime().nullable()
1449
+ });
1385
1450
  z.object({
1386
1451
  systemPrompt: z.string().max(64e3),
1387
1452
  routingInstructions: z.string().max(32e3).nullable(),
@@ -1878,6 +1943,43 @@ z.object({
1878
1943
  perCallMaxCumulativeMs: z.number().int().min(1e3).default(3e5)
1879
1944
  })
1880
1945
  });
1946
+
1947
+ // ../contracts/src/dispatch-metadata.ts
1948
+ var IDENTITY_DISPATCH_METADATA_KEYS = [
1949
+ "projectId",
1950
+ "agentId",
1951
+ "sessionId",
1952
+ "callId",
1953
+ "phoneNumberId",
1954
+ "bindingId",
1955
+ "apiKeyId",
1956
+ "tenantId",
1957
+ // the SecurityPrimitive's identity for the call
1958
+ "moduleId",
1959
+ "toNumber",
1960
+ // fromNumber / toNumber / callerId: a warm transfer's caller ID (sdk handoff.ts ownDidFor)
1961
+ "fromNumber",
1962
+ "callerId"
1963
+ ];
1964
+ var RESERVED_DISPATCH_METADATA_KEYS = [
1965
+ ...IDENTITY_DISPATCH_METADATA_KEYS,
1966
+ "route",
1967
+ "direction",
1968
+ "carrier",
1969
+ "source",
1970
+ "mode",
1971
+ // mode + turnId would make a call's job run as a text turn
1972
+ "turnId",
1973
+ // set by the platform from its own inputs (the call's directive, the project's recording notice, AMD) — a caller's
1974
+ // copy in a metadata bag would speak a line, skip a disclosure or change answering-machine handling
1975
+ "initialDirective",
1976
+ "recordingAnnouncement",
1977
+ "amd"
1978
+ ];
1979
+ new Set(RESERVED_DISPATCH_METADATA_KEYS);
1980
+ new Set(IDENTITY_DISPATCH_METADATA_KEYS);
1981
+
1982
+ // ../contracts/src/flow-validate.ts
1881
1983
  var FlowIssueSeverity = z.enum(["error", "warning"]);
1882
1984
  var FlowIssue = z.object({
1883
1985
  code: z.string().max(64),
@@ -1970,6 +2072,25 @@ z.object({
1970
2072
  amd: OutboundAmdMode.optional()
1971
2073
  });
1972
2074
  var ConnectionAuthType = z.enum(["oauth2", "api_key", "bearer", "basic", "none"]);
2075
+ var TRANSPORT_OWNED_HEADERS = /* @__PURE__ */ new Set([
2076
+ "host",
2077
+ "content-length",
2078
+ "transfer-encoding",
2079
+ "connection",
2080
+ "keep-alive",
2081
+ "upgrade",
2082
+ "te",
2083
+ "trailer",
2084
+ "expect",
2085
+ // the transport advertises only what it can decode
2086
+ "accept-encoding"
2087
+ ]);
2088
+ function isTransportOwnedHeader(name) {
2089
+ const n = name.trim().toLowerCase();
2090
+ return TRANSPORT_OWNED_HEADERS.has(n) || n.startsWith("proxy-");
2091
+ }
2092
+ var PLATFORM_OWNED_HEADERS = { has: (n) => n === "content-type" || isTransportOwnedHeader(n) };
2093
+ z.string().trim().regex(/^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/, "a header name: letters, digits and - only, up to 64").refine((n) => !PLATFORM_OWNED_HEADERS.has(n.toLowerCase()), "the platform sets this header itself");
1973
2094
  var ConnectionStatus = z.enum(["active", "revoked", "error"]);
1974
2095
  z.object({
1975
2096
  v: z.number().int(),
@@ -2055,7 +2176,11 @@ z.object({
2055
2176
  reauthRequired: z.boolean().optional()
2056
2177
  });
2057
2178
  var E164 = z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164 (+15551234567)");
2058
- var RoomPrefix = z.string().regex(/^[a-z0-9][a-z0-9-]{0,30}[a-z0-9-]?$/, "lowercase letters, digits, dashes; max 32");
2179
+ var PLATFORM_ROOM_NAMESPACES = ["text-", "vl-", "call-out-"];
2180
+ var RoomPrefix = z.string().regex(/^[a-z0-9][a-z0-9-]{0,30}[a-z0-9-]?$/, "lowercase letters, digits, dashes; max 32").refine(
2181
+ (v) => !PLATFORM_ROOM_NAMESPACES.some((ns) => v.startsWith(ns)),
2182
+ `must not start with a platform room namespace (${PLATFORM_ROOM_NAMESPACES.join(", ")})`
2183
+ );
2059
2184
  var RecordingPrefix = z.string().min(1).max(128).refine((v) => !v.startsWith("/") && !v.includes(".."), "relative key prefix only");
2060
2185
  var Cidr = z.string().regex(/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/, "must be IPv4 CIDR (a.b.c.d/nn)");
2061
2186
  var TelephonySettings = z.object({
@@ -2356,7 +2481,7 @@ z.discriminatedUnion("kind", [
2356
2481
  BrainRequestFrame,
2357
2482
  BrainCancelFrame
2358
2483
  ]);
2359
- var FLOW_BOOT_FAILURES = ["schema_fetch_failed", "no_process_schema", "missing_agent_id", "no_api_key"];
2484
+ var FLOW_BOOT_FAILURES = ["schema_fetch_failed", "no_process_schema", "missing_agent_id", "no_api_key", "worker_identity_refused"];
2360
2485
  z.enum(
2361
2486
  FLOW_BOOT_FAILURES.map((cause) => `flow_boot:${cause}`)
2362
2487
  );