@alexkroman1/aai 1.7.1 → 1.8.0

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 (133) hide show
  1. package/.turbo/turbo-build.log +11 -9
  2. package/CHANGELOG.md +10 -0
  3. package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
  4. package/dist/constants-y68COEGj.js +29 -0
  5. package/dist/host/_base64.d.ts +2 -0
  6. package/dist/host/_mock-ws.d.ts +0 -61
  7. package/dist/host/_pipeline-test-fakes.d.ts +7 -4
  8. package/dist/host/_run-code.d.ts +0 -25
  9. package/dist/host/_runtime-conformance.d.ts +3 -34
  10. package/dist/host/memory-vector.d.ts +0 -11
  11. package/dist/host/providers/resolve-kv.d.ts +0 -7
  12. package/dist/host/providers/resolve-vector.d.ts +0 -8
  13. package/dist/host/providers/stt/assemblyai.d.ts +0 -14
  14. package/dist/host/providers/stt/deepgram.d.ts +2 -14
  15. package/dist/host/providers/stt/soniox.d.ts +0 -22
  16. package/dist/host/providers/tts/rime.d.ts +10 -31
  17. package/dist/host/runtime-barrel.js +619 -630
  18. package/dist/host/runtime-config.d.ts +9 -6
  19. package/dist/host/runtime.d.ts +3 -0
  20. package/dist/host/to-vercel-tools.d.ts +3 -33
  21. package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
  22. package/dist/host/unstorage-kv.d.ts +0 -26
  23. package/dist/index.js +3 -3
  24. package/dist/openai-realtime-cjPAHMMx.js +10 -0
  25. package/dist/sdk/_internal-types.d.ts +6 -55
  26. package/dist/sdk/allowed-hosts.d.ts +4 -3
  27. package/dist/sdk/constants.d.ts +4 -29
  28. package/dist/sdk/define.d.ts +7 -4
  29. package/dist/sdk/kv.d.ts +13 -37
  30. package/dist/sdk/manifest-barrel.js +1 -1
  31. package/dist/sdk/manifest.d.ts +8 -2
  32. package/dist/sdk/protocol.js +1 -1
  33. package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
  34. package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
  35. package/dist/sdk/providers/s2s-barrel.js +2 -0
  36. package/dist/sdk/providers/tts/rime.d.ts +1 -1
  37. package/dist/sdk/providers.d.ts +6 -2
  38. package/dist/sdk/types.d.ts +7 -1
  39. package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
  40. package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
  41. package/host/_base64.ts +9 -0
  42. package/host/_mock-ws.ts +0 -65
  43. package/host/_pipeline-test-fakes.ts +19 -31
  44. package/host/_run-code.ts +10 -53
  45. package/host/_runtime-conformance.ts +3 -44
  46. package/host/_test-utils.ts +20 -42
  47. package/host/builtin-tools.test.ts +127 -222
  48. package/host/builtin-tools.ts +6 -10
  49. package/host/cleanup.test.ts +30 -73
  50. package/host/integration/pipeline-reference.integration.test.ts +12 -17
  51. package/host/integration.test.ts +0 -7
  52. package/host/memory-vector.test.ts +3 -1
  53. package/host/memory-vector.ts +16 -21
  54. package/host/pinecone-vector.test.ts +14 -17
  55. package/host/pinecone-vector.ts +10 -19
  56. package/host/providers/providers.test-d.ts +5 -3
  57. package/host/providers/resolve-kv.ts +23 -41
  58. package/host/providers/resolve-vector.ts +3 -12
  59. package/host/providers/resolve.test.ts +15 -28
  60. package/host/providers/resolve.ts +24 -24
  61. package/host/providers/stt/assemblyai.test.ts +2 -14
  62. package/host/providers/stt/assemblyai.ts +12 -35
  63. package/host/providers/stt/deepgram.test.ts +23 -83
  64. package/host/providers/stt/deepgram.ts +15 -40
  65. package/host/providers/stt/elevenlabs.test.ts +26 -38
  66. package/host/providers/stt/elevenlabs.ts +10 -9
  67. package/host/providers/stt/soniox.test.ts +35 -85
  68. package/host/providers/stt/soniox.ts +8 -53
  69. package/host/providers/tts/cartesia.test.ts +19 -58
  70. package/host/providers/tts/cartesia.ts +36 -66
  71. package/host/providers/tts/rime.test.ts +12 -38
  72. package/host/providers/tts/rime.ts +23 -86
  73. package/host/runtime-config.test.ts +9 -9
  74. package/host/runtime-config.ts +16 -22
  75. package/host/runtime.test.ts +111 -73
  76. package/host/runtime.ts +138 -86
  77. package/host/s2s.test.ts +92 -191
  78. package/host/s2s.ts +55 -49
  79. package/host/server-shutdown.test.ts +9 -30
  80. package/host/server.test.ts +2 -13
  81. package/host/server.ts +85 -100
  82. package/host/session-core.test.ts +15 -30
  83. package/host/session-core.ts +10 -13
  84. package/host/session-prompt.test.ts +1 -5
  85. package/host/to-vercel-tools.test.ts +53 -72
  86. package/host/to-vercel-tools.ts +9 -39
  87. package/host/tool-executor.test.ts +25 -51
  88. package/host/tool-executor.ts +18 -12
  89. package/host/transports/openai-realtime-transport.test.ts +371 -0
  90. package/host/transports/openai-realtime-transport.ts +319 -0
  91. package/host/transports/pipeline-transport.test.ts +125 -298
  92. package/host/transports/pipeline-transport.ts +20 -68
  93. package/host/transports/s2s-transport-fixtures.test.ts +31 -92
  94. package/host/transports/s2s-transport.test.ts +65 -134
  95. package/host/transports/s2s-transport.ts +15 -43
  96. package/host/transports/types.test.ts +4 -8
  97. package/host/unstorage-kv.test.ts +3 -2
  98. package/host/unstorage-kv.ts +5 -35
  99. package/host/ws-handler.test.ts +72 -176
  100. package/host/ws-handler.ts +6 -12
  101. package/package.json +6 -1
  102. package/sdk/__snapshots__/exports.test.ts.snap +7 -0
  103. package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
  104. package/sdk/_internal-types.test.ts +6 -9
  105. package/sdk/_internal-types.ts +16 -57
  106. package/sdk/_test-matchers.ts +25 -15
  107. package/sdk/allowed-hosts.test.ts +50 -114
  108. package/sdk/allowed-hosts.ts +8 -14
  109. package/sdk/constants.ts +5 -52
  110. package/sdk/define.test.ts +7 -6
  111. package/sdk/define.ts +7 -3
  112. package/sdk/exports.test.ts +6 -1
  113. package/sdk/kv.ts +13 -37
  114. package/sdk/manifest.test-d.ts +5 -0
  115. package/sdk/manifest.test.ts +61 -9
  116. package/sdk/manifest.ts +11 -11
  117. package/sdk/protocol-compat.test.ts +66 -98
  118. package/sdk/protocol-snapshot.test.ts +2 -16
  119. package/sdk/protocol.test.ts +13 -22
  120. package/sdk/providers/s2s/openai-realtime.ts +36 -0
  121. package/sdk/providers/s2s-barrel.ts +12 -0
  122. package/sdk/providers/tts/rime.ts +1 -1
  123. package/sdk/providers.ts +24 -5
  124. package/sdk/schema-alignment.test.ts +25 -73
  125. package/sdk/schema-shapes.test.ts +1 -29
  126. package/sdk/system-prompt.test.ts +0 -1
  127. package/sdk/system-prompt.ts +17 -19
  128. package/sdk/types-inference.test.ts +10 -36
  129. package/sdk/types.ts +7 -0
  130. package/sdk/ws-upgrade.test.ts +24 -23
  131. package/sdk/ws-upgrade.ts +2 -3
  132. package/tsdown.config.ts +8 -11
  133. package/dist/constants-C2nirZUI.js +0 -54
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -47,6 +47,11 @@
47
47
  "@dev/source": "./sdk/providers/kv-barrel.ts",
48
48
  "types": "./dist/sdk/providers/kv-barrel.d.ts",
49
49
  "import": "./dist/sdk/providers/kv-barrel.js"
50
+ },
51
+ "./s2s": {
52
+ "@dev/source": "./sdk/providers/s2s-barrel.ts",
53
+ "types": "./dist/sdk/providers/s2s-barrel.d.ts",
54
+ "import": "./dist/sdk/providers/s2s-barrel.js"
50
55
  }
51
56
  },
52
57
  "dependencies": {
@@ -89,3 +89,10 @@ exports[`export surface stability > @alexkroman1/aai/runtime export 1`] = `
89
89
  "wireSessionSocket",
90
90
  ]
91
91
  `;
92
+
93
+ exports[`export surface stability > @alexkroman1/aai/s2s export 1`] = `
94
+ [
95
+ "OPENAI_REALTIME_KIND",
96
+ "openaiRealtime",
97
+ ]
98
+ `;
@@ -10,6 +10,7 @@ exports[`manifest schema shapes > AgentConfigSchema shape 1`] = `
10
10
  "maxSteps",
11
11
  "mode",
12
12
  "name",
13
+ "s2s",
13
14
  "stt",
14
15
  "sttPrompt",
15
16
  "systemPrompt",
@@ -5,15 +5,14 @@ import { agentToolsToSchemas } from "./_internal-types.ts";
5
5
  import type { ToolDef } from "./types.ts";
6
6
 
7
7
  test("agentToolsToSchemas - converts tool definitions to OpenAI schema", () => {
8
+ const noop = async () => {
9
+ /* no-op */
10
+ };
8
11
  const tools: Record<string, ToolDef> = {
9
12
  get_weather: {
10
13
  description: "Get weather",
11
- parameters: z.object({
12
- city: z.string().describe("City"),
13
- }),
14
- execute: async () => {
15
- /* noop */
16
- },
14
+ parameters: z.object({ city: z.string().describe("City") }),
15
+ execute: noop,
17
16
  },
18
17
  set_alarm: {
19
18
  description: "Set alarm",
@@ -21,9 +20,7 @@ test("agentToolsToSchemas - converts tool definitions to OpenAI schema", () => {
21
20
  time: z.string(),
22
21
  label: z.string().optional(),
23
22
  }),
24
- execute: async () => {
25
- /* noop */
26
- },
23
+ execute: noop,
27
24
  },
28
25
  };
29
26
  const schemas = agentToolsToSchemas(tools);
@@ -1,9 +1,4 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
- /**
3
- * Internal type definitions shared by server and CLI.
4
- *
5
- * Note: this module is for internal use only and should not be used directly.
6
- */
7
2
 
8
3
  import type { JSONSchema7 } from "json-schema";
9
4
  import { z } from "zod";
@@ -12,6 +7,7 @@ import {
12
7
  assertProviderTriple,
13
8
  type KvProvider,
14
9
  type LlmProvider,
10
+ type S2sProvider,
15
11
  type SttProvider,
16
12
  type TtsProvider,
17
13
  type VectorProvider,
@@ -19,27 +15,11 @@ import {
19
15
  import type { Message } from "./types.ts";
20
16
  import { BuiltinToolSchema, ToolChoiceSchema, type ToolDef } from "./types.ts";
21
17
 
22
- /**
23
- * Options forwarded to an {@link ExecuteTool} invocation.
24
- *
25
- * Primarily used by the pipeline orchestrator (streamText tool loop) to
26
- * thread an {@link AbortSignal} into tool execution. The S2S voice path
27
- * does not pass these options today — recipients must treat the whole
28
- * bag as optional.
29
- */
30
18
  export interface ExecuteToolOptions {
31
- /** Abort signal bound to the enclosing LLM turn / request. */
32
19
  signal?: AbortSignal;
33
- /** Vercel AI SDK tool-call ID for this invocation. Useful for tracing and correlation. */
34
20
  toolCallId?: string;
35
21
  }
36
22
 
37
- /**
38
- * Function signature for executing a tool by name.
39
- *
40
- * Used by session.ts to invoke tools, by direct-executor.ts and
41
- * harness-runtime.ts to implement the execution.
42
- */
43
23
  export type ExecuteTool = (
44
24
  name: string,
45
25
  args: Readonly<Record<string, unknown>>,
@@ -50,12 +30,8 @@ export type ExecuteTool = (
50
30
 
51
31
  // ─── AgentConfig ────────────────────────────────────────────────────────────
52
32
 
53
- /**
54
- * Zod schema for serializable agent configuration sent over the wire.
55
- *
56
- * This is the JSON-safe subset of the agent definition that can be
57
- * transmitted between the worker and the host process via structured clone.
58
- */
33
+ // JSON-safe subset of the agent definition, transmitted between worker and
34
+ // host via structured clone.
59
35
  export const AgentConfigSchema = z.object({
60
36
  name: z.string().min(1),
61
37
  systemPrompt: z.string(),
@@ -68,19 +44,16 @@ export const AgentConfigSchema = z.object({
68
44
  stt: ProviderDescriptorSchema.optional(),
69
45
  llm: ProviderDescriptorSchema.optional(),
70
46
  tts: ProviderDescriptorSchema.optional(),
47
+ s2s: ProviderDescriptorSchema.optional(),
71
48
  mode: z.enum(["s2s", "pipeline"]).optional(),
72
49
  kv: ProviderDescriptorSchema.optional(),
73
50
  vector: ProviderDescriptorSchema.optional(),
74
51
  });
75
52
 
76
- /** Serializable agent configuration — derived from {@link AgentConfigSchema}. */
77
53
  export type AgentConfig = z.infer<typeof AgentConfigSchema>;
78
54
 
79
- /**
80
- * Input shape accepted by {@link toAgentConfig}. Covers both `AgentDef`
81
- * (where `maxSteps` may be a function) and `IsolateConfig` (where it is
82
- * always a number).
83
- */
55
+ // Covers both `AgentDef` (where `maxSteps` may be a function) and
56
+ // `IsolateConfig` (where it is always a number).
84
57
  export interface AgentConfigSource {
85
58
  name: string;
86
59
  systemPrompt: string;
@@ -93,35 +66,33 @@ export interface AgentConfigSource {
93
66
  stt?: SttProvider | undefined;
94
67
  llm?: LlmProvider | undefined;
95
68
  tts?: TtsProvider | undefined;
69
+ s2s?: S2sProvider | undefined;
96
70
  kv?: KvProvider | undefined;
97
71
  vector?: VectorProvider | undefined;
98
72
  }
99
73
 
100
- /**
101
- * Extract the serializable {@link AgentConfig} subset from a source object.
102
- *
103
- * When `stt`, `llm`, and `tts` descriptors are present they are all three
104
- * required (or none) — enforced here so the server can trust the config.
105
- * `mode` is derived from their presence.
106
- */
107
74
  export function toAgentConfig(src: AgentConfigSource): AgentConfig {
75
+ // `assertProviderTriple` enforces that stt/llm/tts are all-or-nothing so the
76
+ // server can trust the resolved mode.
77
+ const mode = assertProviderTriple(src.stt, src.llm, src.tts, src.s2s);
78
+
108
79
  const config: AgentConfig = {
109
80
  name: src.name,
110
81
  systemPrompt: src.systemPrompt,
111
82
  greeting: src.greeting,
83
+ mode,
112
84
  };
113
85
  if (src.sttPrompt !== undefined) config.sttPrompt = src.sttPrompt;
114
86
  if (src.maxSteps !== undefined) config.maxSteps = src.maxSteps;
115
87
  if (src.toolChoice !== undefined) config.toolChoice = src.toolChoice;
116
88
  if (src.builtinTools) config.builtinTools = [...src.builtinTools];
117
89
  if (src.idleTimeoutMs !== undefined) config.idleTimeoutMs = src.idleTimeoutMs;
118
-
119
- config.mode = assertProviderTriple(src.stt, src.llm, src.tts);
120
- if (config.mode === "pipeline") {
90
+ if (mode === "pipeline") {
121
91
  config.stt = src.stt;
122
92
  config.llm = src.llm;
123
93
  config.tts = src.tts;
124
94
  }
95
+ if (src.s2s !== undefined) config.s2s = src.s2s;
125
96
  if (src.kv !== undefined) config.kv = src.kv;
126
97
  if (src.vector !== undefined) config.vector = src.vector;
127
98
  return config;
@@ -129,12 +100,8 @@ export function toAgentConfig(src: AgentConfigSource): AgentConfig {
129
100
 
130
101
  // ─── ToolSchema ─────────────────────────────────────────────────────────────
131
102
 
132
- /**
133
- * Zod schema for serialized tool definitions sent over the wire.
134
- *
135
- * `parameters` must be a valid JSON Schema object (with `type`, `properties`,
136
- * etc.) — the Vercel AI SDK wraps it via `jsonSchema()`.
137
- */
103
+ // `parameters` must be a valid JSON Schema object — the Vercel AI SDK wraps
104
+ // it via `jsonSchema()`.
138
105
  export const ToolSchemaSchema = z.object({
139
106
  type: z.literal("function"),
140
107
  name: z.string().min(1),
@@ -142,7 +109,6 @@ export const ToolSchemaSchema = z.object({
142
109
  parameters: z.record(z.string(), z.unknown()),
143
110
  });
144
111
 
145
- /** Serialized tool schema — derived from {@link ToolSchemaSchema}. */
146
112
  export type ToolSchema = {
147
113
  type: "function";
148
114
  name: string;
@@ -150,15 +116,8 @@ export type ToolSchema = {
150
116
  parameters: JSONSchema7;
151
117
  };
152
118
 
153
- /** Empty Zod object schema used as default when tools have no parameters. */
154
119
  export const EMPTY_PARAMS = z.object({});
155
120
 
156
- /**
157
- * Convert agent tool definitions to JSON Schema format for wire transport.
158
- *
159
- * Transforms the Zod-based `parameters` of each tool into a plain JSON Schema
160
- * object suitable for structured clone / JSON serialization.
161
- */
162
121
  export function agentToolsToSchemas(tools: Readonly<Record<string, ToolDef>>): ToolSchema[] {
163
122
  return Object.entries(tools).map(([name, def]) => ({
164
123
  type: "function",
@@ -12,16 +12,24 @@ import { ClientEventSchema } from "./protocol.ts";
12
12
 
13
13
  type MatcherResult = { pass: boolean; message: () => string };
14
14
 
15
- // ─── Matcher implementations ────────────────────────────────────────────────
15
+ type EventLike = { type?: unknown; [key: string]: unknown };
16
+
17
+ function fieldsSuffix(fields?: Record<string, unknown>): string {
18
+ return fields ? ` with fields ${JSON.stringify(fields)}` : "";
19
+ }
16
20
 
17
21
  function toBeValidClientEvent(received: unknown): MatcherResult {
18
22
  const result = ClientEventSchema.safeParse(received);
23
+ if (result.success) {
24
+ return {
25
+ pass: true,
26
+ message: () => "expected value NOT to be a valid ClientEvent, but it parsed successfully",
27
+ };
28
+ }
29
+ const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
19
30
  return {
20
- pass: result.success,
21
- message: () =>
22
- result.success
23
- ? "expected value NOT to be a valid ClientEvent, but it parsed successfully"
24
- : `expected value to be a valid ClientEvent\n\nZod errors:\n${result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}`,
31
+ pass: false,
32
+ message: () => `expected value to be a valid ClientEvent\n\nZod errors:\n${issues}`,
25
33
  };
26
34
  }
27
35
 
@@ -37,30 +45,32 @@ function toContainEvent(
37
45
  };
38
46
  }
39
47
 
40
- const match = received.some((event: Record<string, unknown>) => {
48
+ const events = received as EventLike[];
49
+ const match = events.some((event) => {
41
50
  if (event?.type !== type) return false;
42
51
  if (!fields) return true;
43
52
  return Object.entries(fields).every(([key, value]) => isDeepStrictEqual(event[key], value));
44
53
  });
45
54
 
55
+ if (match) {
56
+ return {
57
+ pass: true,
58
+ message: () => `expected array NOT to contain event of type "${type}"${fieldsSuffix(fields)}`,
59
+ };
60
+ }
61
+ const receivedTypes = events.map((e) => `"${e?.type}"`).join(", ");
46
62
  return {
47
- pass: match,
63
+ pass: false,
48
64
  message: () =>
49
- match
50
- ? `expected array NOT to contain event of type "${type}"${fields ? ` with fields ${JSON.stringify(fields)}` : ""}`
51
- : `expected array to contain event of type "${type}"${fields ? ` with fields ${JSON.stringify(fields)}` : ""}\n\nReceived event types: [${received.map((e: Record<string, unknown>) => `"${e?.type}"`).join(", ")}]`,
65
+ `expected array to contain event of type "${type}"${fieldsSuffix(fields)}\n\nReceived event types: [${receivedTypes}]`,
52
66
  };
53
67
  }
54
68
 
55
- // ─── Register matchers ──────────────────────────────────────────────────────
56
-
57
69
  expect.extend({
58
70
  toBeValidClientEvent,
59
71
  toContainEvent,
60
72
  });
61
73
 
62
- // ─── Type augmentation ──────────────────────────────────────────────────────
63
-
64
74
  declare module "vitest" {
65
75
  interface Assertion<T> {
66
76
  toBeValidClientEvent(): void;
@@ -2,153 +2,89 @@
2
2
  import { describe, expect, test } from "vitest";
3
3
  import { matchesAllowedHost, validateAllowedHostPattern } from "./allowed-hosts.ts";
4
4
 
5
+ function expectInvalid(pattern: string, reasonPattern?: RegExp): void {
6
+ const result = validateAllowedHostPattern(pattern);
7
+ // biome-ignore lint/suspicious/noMisplacedAssertion: shared assertion helper used inside test()
8
+ expect(result.valid).toBe(false);
9
+ if (reasonPattern) {
10
+ // biome-ignore lint/suspicious/noMisplacedAssertion: shared assertion helper used inside test()
11
+ expect((result as { valid: false; reason: string }).reason).toMatch(reasonPattern);
12
+ }
13
+ }
14
+
5
15
  describe("validateAllowedHostPattern", () => {
6
16
  describe("valid patterns", () => {
7
- test("accepts exact hostname", () => {
8
- expect(validateAllowedHostPattern("api.weather.com")).toEqual({
9
- valid: true,
10
- });
11
- });
12
-
13
- test("accepts exact hostname without subdomain", () => {
14
- expect(validateAllowedHostPattern("example.com")).toEqual({ valid: true });
15
- });
16
-
17
- test("accepts wildcard subdomain", () => {
18
- expect(validateAllowedHostPattern("*.mycompany.com")).toEqual({
19
- valid: true,
20
- });
21
- });
22
-
23
- test("accepts wildcard with multiple domain levels", () => {
24
- expect(validateAllowedHostPattern("*.api.mycompany.com")).toEqual({
25
- valid: true,
26
- });
17
+ test.each([
18
+ ["api.weather.com"],
19
+ ["example.com"],
20
+ ["*.mycompany.com"],
21
+ ["*.api.mycompany.com"],
22
+ ])("accepts %s", (pattern) => {
23
+ expect(validateAllowedHostPattern(pattern)).toEqual({ valid: true });
27
24
  });
28
25
  });
29
26
 
30
27
  describe("rejects bare wildcards", () => {
31
28
  test("rejects bare *", () => {
32
- const result = validateAllowedHostPattern("*");
33
- expect(result.valid).toBe(false);
34
- expect((result as { valid: false; reason: string }).reason).toMatch(/bare/i);
29
+ expectInvalid("*", /bare/i);
35
30
  });
36
31
 
37
32
  test("rejects bare **", () => {
38
- const result = validateAllowedHostPattern("**");
39
- expect(result.valid).toBe(false);
33
+ expectInvalid("**");
40
34
  });
41
35
  });
42
36
 
43
37
  describe("rejects wildcard in non-leading position", () => {
44
- test("rejects wildcard in middle position", () => {
45
- const result = validateAllowedHostPattern("api.*.com");
46
- expect(result.valid).toBe(false);
47
- });
48
-
49
- test("rejects wildcard at end", () => {
50
- const result = validateAllowedHostPattern("api.com.*");
51
- expect(result.valid).toBe(false);
38
+ test.each([["api.*.com"], ["api.com.*"]])("rejects %s", (pattern) => {
39
+ expectInvalid(pattern);
52
40
  });
53
41
  });
54
42
 
55
43
  describe("rejects IP addresses", () => {
56
- test("rejects IPv4 address", () => {
57
- const result = validateAllowedHostPattern("192.168.1.1");
58
- expect(result.valid).toBe(false);
59
- expect((result as { valid: false; reason: string }).reason).toMatch(/ip/i);
60
- });
61
-
62
- test("rejects IPv4 loopback", () => {
63
- const result = validateAllowedHostPattern("127.0.0.1");
64
- expect(result.valid).toBe(false);
65
- });
66
-
67
- test("rejects IPv6 address", () => {
68
- const result = validateAllowedHostPattern("::1");
69
- expect(result.valid).toBe(false);
70
- expect((result as { valid: false; reason: string }).reason).toMatch(/ip/i);
71
- });
72
-
73
- test("rejects full IPv6 address", () => {
74
- const result = validateAllowedHostPattern("2001:db8::1");
75
- expect(result.valid).toBe(false);
76
- expect((result as { valid: false; reason: string }).reason).toMatch(/ip/i);
44
+ test.each([
45
+ ["192.168.1.1", /ip/i],
46
+ ["127.0.0.1", undefined],
47
+ ["::1", /ip/i],
48
+ ["2001:db8::1", /ip/i],
49
+ ])("rejects %s", (pattern, reason) => {
50
+ expectInvalid(pattern, reason);
77
51
  });
78
52
  });
79
53
 
80
54
  describe("rejects private TLDs", () => {
81
- test("rejects *.local", () => {
82
- const result = validateAllowedHostPattern("*.local");
83
- expect(result.valid).toBe(false);
84
- });
85
-
86
- test("rejects *.internal", () => {
87
- const result = validateAllowedHostPattern("*.internal");
88
- expect(result.valid).toBe(false);
89
- });
90
-
91
- test("rejects *.localhost", () => {
92
- const result = validateAllowedHostPattern("*.localhost");
93
- expect(result.valid).toBe(false);
94
- });
95
-
96
- test("rejects exact match foo.local", () => {
97
- const result = validateAllowedHostPattern("foo.local");
98
- expect(result.valid).toBe(false);
99
- });
100
-
101
- test("rejects exact match foo.internal", () => {
102
- const result = validateAllowedHostPattern("foo.internal");
103
- expect(result.valid).toBe(false);
104
- });
105
-
106
- test("rejects exact match foo.localhost", () => {
107
- const result = validateAllowedHostPattern("foo.localhost");
108
- expect(result.valid).toBe(false);
55
+ test.each([
56
+ ["*.local"],
57
+ ["*.internal"],
58
+ ["*.localhost"],
59
+ ["foo.local"],
60
+ ["foo.internal"],
61
+ ["foo.localhost"],
62
+ ])("rejects %s", (pattern) => {
63
+ expectInvalid(pattern);
109
64
  });
110
65
  });
111
66
 
112
67
  describe("rejects cloud metadata hostnames", () => {
113
- test("rejects metadata.google.internal", () => {
114
- const result = validateAllowedHostPattern("metadata.google.internal");
115
- expect(result.valid).toBe(false);
116
- });
117
-
118
- test("rejects instance-data.ec2.internal", () => {
119
- const result = validateAllowedHostPattern("instance-data.ec2.internal");
120
- expect(result.valid).toBe(false);
68
+ test.each([
69
+ ["metadata.google.internal"],
70
+ ["instance-data.ec2.internal"],
71
+ ])("rejects %s", (pattern) => {
72
+ expectInvalid(pattern);
121
73
  });
122
74
  });
123
75
 
124
76
  describe("rejects empty and malformed patterns", () => {
125
77
  test("rejects empty string", () => {
126
- const result = validateAllowedHostPattern("");
127
- expect(result.valid).toBe(false);
128
- });
129
-
130
- test("rejects pattern with protocol", () => {
131
- const result = validateAllowedHostPattern("https://api.example.com");
132
- expect(result.valid).toBe(false);
133
- expect((result as { valid: false; reason: string }).reason).toMatch(/protocol/i);
134
- });
135
-
136
- test("rejects pattern with path", () => {
137
- const result = validateAllowedHostPattern("api.example.com/path");
138
- expect(result.valid).toBe(false);
139
- expect((result as { valid: false; reason: string }).reason).toMatch(/path/i);
140
- });
141
-
142
- test("rejects pattern with query", () => {
143
- const result = validateAllowedHostPattern("api.example.com?query=1");
144
- expect(result.valid).toBe(false);
145
- expect((result as { valid: false; reason: string }).reason).toMatch(/query/i);
78
+ expectInvalid("");
146
79
  });
147
80
 
148
- test("rejects pattern with port", () => {
149
- const result = validateAllowedHostPattern("api.example.com:8080");
150
- expect(result.valid).toBe(false);
151
- expect((result as { valid: false; reason: string }).reason).toMatch(/port/i);
81
+ test.each([
82
+ ["https://api.example.com", /protocol/i],
83
+ ["api.example.com/path", /path/i],
84
+ ["api.example.com?query=1", /query/i],
85
+ ["api.example.com:8080", /port/i],
86
+ ])("rejects %s", (pattern, reason) => {
87
+ expectInvalid(pattern, reason);
152
88
  });
153
89
  });
154
90
  });
@@ -9,18 +9,14 @@
9
9
  * environment (browser, Deno, Node.js sandboxes).
10
10
  */
11
11
 
12
- /** Private/special-use TLDs that must never appear in allowedHosts patterns. */
13
12
  const BLOCKED_TLDS = ["local", "internal", "localhost"];
14
13
 
15
- /**
16
- * Regex that matches an IPv4 address (four decimal octets separated by dots).
17
- * Anchored so partial matches like "192.168.1.1.example.com" don't trigger it.
18
- */
19
14
  const IPV4_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
20
15
 
21
- type ValidationResult = { valid: true } | { valid: false; reason: string };
16
+ type ValidationFailure = { valid: false; reason: string };
17
+ type ValidationResult = { valid: true } | ValidationFailure;
22
18
 
23
- function fail(reason: string): { valid: false; reason: string } {
19
+ function fail(reason: string): ValidationFailure {
24
20
  return { valid: false, reason };
25
21
  }
26
22
 
@@ -91,18 +87,16 @@ export function validateAllowedHostPattern(pattern: string): ValidationResult {
91
87
  export function matchesAllowedHost(hostname: string, patterns: string[]): boolean {
92
88
  if (patterns.length === 0) return false;
93
89
 
94
- // Strip port only when there are no brackets (IPv6 bracket notation).
90
+ // Strip port only when there are no brackets (preserves IPv6 bracket notation).
95
91
  const portIndex = hostname.lastIndexOf(":");
96
- let host = portIndex !== -1 && !hostname.includes("[") ? hostname.slice(0, portIndex) : hostname;
97
-
98
- // Normalise: lowercase + strip trailing dot
99
- host = host.toLowerCase().replace(/\.$/, "");
92
+ const withoutPort =
93
+ portIndex !== -1 && !hostname.includes("[") ? hostname.slice(0, portIndex) : hostname;
94
+ const host = withoutPort.toLowerCase().replace(/\.$/, "");
100
95
 
101
96
  for (const pattern of patterns) {
102
97
  const p = pattern.toLowerCase();
103
98
  if (p.startsWith("*.")) {
104
- // Wildcard: hostname must end with the suffix (e.g. ".example.com")
105
- const suffix = p.slice(1); // ".example.com"
99
+ const suffix = p.slice(1);
106
100
  if (host.endsWith(suffix) && host.length > suffix.length) return true;
107
101
  } else if (host === p) {
108
102
  return true;
package/sdk/constants.ts CHANGED
@@ -1,81 +1,34 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
- /**
3
- * Centralised numeric constants — timeouts, size limits, sample rates.
4
- *
5
- * Every magic number that controls a timeout, buffer size, or threshold
6
- * lives here so the values are discoverable in one place.
7
- */
8
-
9
- // ─── Audio ────────────────────────────────────────────────────────────────
10
2
 
11
- /** Default sample rate for speech-to-text audio in Hz (AssemblyAI). */
12
3
  export const DEFAULT_STT_SAMPLE_RATE = 16_000;
13
-
14
- /** Default sample rate for text-to-speech audio in Hz. */
15
4
  export const DEFAULT_TTS_SAMPLE_RATE = 24_000;
16
5
 
17
- // ─── Timeouts (ms) ───────────────────────────────────────────────────────
18
-
19
- /** Default timeout for tool execution in the worker. */
20
6
  export const TOOL_EXECUTION_TIMEOUT_MS = 30_000;
21
-
22
- /** Timeout for session.start() (S2S connection setup). */
23
7
  export const DEFAULT_SESSION_START_TIMEOUT_MS = 10_000;
24
-
25
- /** S2S session idle timeout before auto-close. */
26
- export const DEFAULT_IDLE_TIMEOUT_MS = 300_000; // 5 minutes
27
-
28
- /** Per-fetch timeout for network tools (web_search, visit_webpage, fetch_json). */
8
+ export const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
29
9
  export const FETCH_TIMEOUT_MS = 15_000;
30
-
31
- /** Timeout for sandboxed run_code execution. */
32
10
  export const RUN_CODE_TIMEOUT_MS = 5000;
33
-
34
- /** Maximum time to wait for sessions to stop during graceful shutdown. */
35
11
  export const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
36
12
 
37
13
  /**
38
- * Maximum time to wait for a pipeline-mode TTS drain after `flush()` before
39
- * forcing the turn to complete. Prevents a stuck TTS provider from wedging
40
- * the session. Short relative to `DEFAULT_SHUTDOWN_TIMEOUT_MS` so stop()
41
- * can still reclaim the socket cleanly.
14
+ * Short relative to `DEFAULT_SHUTDOWN_TIMEOUT_MS` so a stuck TTS provider
15
+ * can't wedge the session stop() must still reclaim the socket cleanly.
42
16
  */
43
17
  export const PIPELINE_FLUSH_TIMEOUT_MS = 10_000;
44
18
 
45
- // ─── Size / length limits ────────────────────────────────────────────────
46
-
47
- /** Maximum length for tool result strings sent to clients. */
48
19
  export const MAX_TOOL_RESULT_CHARS = 4000;
49
-
50
- /** Maximum chars for webpage text after HTML-to-text conversion. */
51
20
  export const MAX_PAGE_CHARS = 10_000;
52
-
53
- /** Maximum bytes to fetch from an HTML page before conversion. */
54
21
  export const MAX_HTML_BYTES = 200_000;
55
-
56
- /** Maximum value size for KV store entries (bytes). */
57
22
  export const MAX_VALUE_SIZE = 65_536;
58
-
59
- /** Maximum conversation messages to retain (sliding window). */
60
23
  export const DEFAULT_MAX_HISTORY = 200;
61
-
62
- /** Maximum WebSocket message payload size (bytes, 1 MiB). */
63
24
  export const MAX_WS_PAYLOAD_BYTES = 1 * 1024 * 1024;
64
-
65
- /** Maximum messages buffered while session.start() is pending. */
66
25
  export const MAX_MESSAGE_BUFFER_SIZE = 100;
67
26
 
68
- // ─── WebSocket ──────────────────────────────────────────────────────────
69
-
70
- /** WebSocket readyState value indicating the connection is open. */
71
27
  export const WS_OPEN = 1;
72
28
 
73
- // ─── Security ───────────────────────────────────────────────────────────
74
-
75
29
  /**
76
- * Content-Security-Policy applied to agent UI pages (both self-hosted and
77
- * platform). Single source of truth used by `secureHeaders` middleware
78
- * and per-response CSP headers.
30
+ * Single source of truth used by `secureHeaders` middleware and
31
+ * per-response CSP headers across self-hosted and platform agent UIs.
79
32
  */
80
33
  export const AGENT_CSP =
81
34
  "default-src 'self'; script-src 'self' 'unsafe-eval' blob:; " +