@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.
- package/.turbo/turbo-build.log +11 -9
- package/CHANGELOG.md +10 -0
- package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
- package/dist/constants-y68COEGj.js +29 -0
- package/dist/host/_base64.d.ts +2 -0
- package/dist/host/_mock-ws.d.ts +0 -61
- package/dist/host/_pipeline-test-fakes.d.ts +7 -4
- package/dist/host/_run-code.d.ts +0 -25
- package/dist/host/_runtime-conformance.d.ts +3 -34
- package/dist/host/memory-vector.d.ts +0 -11
- package/dist/host/providers/resolve-kv.d.ts +0 -7
- package/dist/host/providers/resolve-vector.d.ts +0 -8
- package/dist/host/providers/stt/assemblyai.d.ts +0 -14
- package/dist/host/providers/stt/deepgram.d.ts +2 -14
- package/dist/host/providers/stt/soniox.d.ts +0 -22
- package/dist/host/providers/tts/rime.d.ts +10 -31
- package/dist/host/runtime-barrel.js +619 -630
- package/dist/host/runtime-config.d.ts +9 -6
- package/dist/host/runtime.d.ts +3 -0
- package/dist/host/to-vercel-tools.d.ts +3 -33
- package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
- package/dist/host/unstorage-kv.d.ts +0 -26
- package/dist/index.js +3 -3
- package/dist/openai-realtime-cjPAHMMx.js +10 -0
- package/dist/sdk/_internal-types.d.ts +6 -55
- package/dist/sdk/allowed-hosts.d.ts +4 -3
- package/dist/sdk/constants.d.ts +4 -29
- package/dist/sdk/define.d.ts +7 -4
- package/dist/sdk/kv.d.ts +13 -37
- package/dist/sdk/manifest-barrel.js +1 -1
- package/dist/sdk/manifest.d.ts +8 -2
- package/dist/sdk/protocol.js +1 -1
- package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
- package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
- package/dist/sdk/providers/s2s-barrel.js +2 -0
- package/dist/sdk/providers/tts/rime.d.ts +1 -1
- package/dist/sdk/providers.d.ts +6 -2
- package/dist/sdk/types.d.ts +7 -1
- package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
- package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
- package/host/_base64.ts +9 -0
- package/host/_mock-ws.ts +0 -65
- package/host/_pipeline-test-fakes.ts +19 -31
- package/host/_run-code.ts +10 -53
- package/host/_runtime-conformance.ts +3 -44
- package/host/_test-utils.ts +20 -42
- package/host/builtin-tools.test.ts +127 -222
- package/host/builtin-tools.ts +6 -10
- package/host/cleanup.test.ts +30 -73
- package/host/integration/pipeline-reference.integration.test.ts +12 -17
- package/host/integration.test.ts +0 -7
- package/host/memory-vector.test.ts +3 -1
- package/host/memory-vector.ts +16 -21
- package/host/pinecone-vector.test.ts +14 -17
- package/host/pinecone-vector.ts +10 -19
- package/host/providers/providers.test-d.ts +5 -3
- package/host/providers/resolve-kv.ts +23 -41
- package/host/providers/resolve-vector.ts +3 -12
- package/host/providers/resolve.test.ts +15 -28
- package/host/providers/resolve.ts +24 -24
- package/host/providers/stt/assemblyai.test.ts +2 -14
- package/host/providers/stt/assemblyai.ts +12 -35
- package/host/providers/stt/deepgram.test.ts +23 -83
- package/host/providers/stt/deepgram.ts +15 -40
- package/host/providers/stt/elevenlabs.test.ts +26 -38
- package/host/providers/stt/elevenlabs.ts +10 -9
- package/host/providers/stt/soniox.test.ts +35 -85
- package/host/providers/stt/soniox.ts +8 -53
- package/host/providers/tts/cartesia.test.ts +19 -58
- package/host/providers/tts/cartesia.ts +36 -66
- package/host/providers/tts/rime.test.ts +12 -38
- package/host/providers/tts/rime.ts +23 -86
- package/host/runtime-config.test.ts +9 -9
- package/host/runtime-config.ts +16 -22
- package/host/runtime.test.ts +111 -73
- package/host/runtime.ts +138 -86
- package/host/s2s.test.ts +92 -191
- package/host/s2s.ts +55 -49
- package/host/server-shutdown.test.ts +9 -30
- package/host/server.test.ts +2 -13
- package/host/server.ts +85 -100
- package/host/session-core.test.ts +15 -30
- package/host/session-core.ts +10 -13
- package/host/session-prompt.test.ts +1 -5
- package/host/to-vercel-tools.test.ts +53 -72
- package/host/to-vercel-tools.ts +9 -39
- package/host/tool-executor.test.ts +25 -51
- package/host/tool-executor.ts +18 -12
- package/host/transports/openai-realtime-transport.test.ts +371 -0
- package/host/transports/openai-realtime-transport.ts +319 -0
- package/host/transports/pipeline-transport.test.ts +125 -298
- package/host/transports/pipeline-transport.ts +20 -68
- package/host/transports/s2s-transport-fixtures.test.ts +31 -92
- package/host/transports/s2s-transport.test.ts +65 -134
- package/host/transports/s2s-transport.ts +15 -43
- package/host/transports/types.test.ts +4 -8
- package/host/unstorage-kv.test.ts +3 -2
- package/host/unstorage-kv.ts +5 -35
- package/host/ws-handler.test.ts +72 -176
- package/host/ws-handler.ts +6 -12
- package/package.json +6 -1
- package/sdk/__snapshots__/exports.test.ts.snap +7 -0
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
- package/sdk/_internal-types.test.ts +6 -9
- package/sdk/_internal-types.ts +16 -57
- package/sdk/_test-matchers.ts +25 -15
- package/sdk/allowed-hosts.test.ts +50 -114
- package/sdk/allowed-hosts.ts +8 -14
- package/sdk/constants.ts +5 -52
- package/sdk/define.test.ts +7 -6
- package/sdk/define.ts +7 -3
- package/sdk/exports.test.ts +6 -1
- package/sdk/kv.ts +13 -37
- package/sdk/manifest.test-d.ts +5 -0
- package/sdk/manifest.test.ts +61 -9
- package/sdk/manifest.ts +11 -11
- package/sdk/protocol-compat.test.ts +66 -98
- package/sdk/protocol-snapshot.test.ts +2 -16
- package/sdk/protocol.test.ts +13 -22
- package/sdk/providers/s2s/openai-realtime.ts +36 -0
- package/sdk/providers/s2s-barrel.ts +12 -0
- package/sdk/providers/tts/rime.ts +1 -1
- package/sdk/providers.ts +24 -5
- package/sdk/schema-alignment.test.ts +25 -73
- package/sdk/schema-shapes.test.ts +1 -29
- package/sdk/system-prompt.test.ts +0 -1
- package/sdk/system-prompt.ts +17 -19
- package/sdk/types-inference.test.ts +10 -36
- package/sdk/types.ts +7 -0
- package/sdk/ws-upgrade.test.ts +24 -23
- package/sdk/ws-upgrade.ts +2 -3
- package/tsdown.config.ts +8 -11
- 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.
|
|
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": {
|
|
@@ -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
|
-
|
|
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:
|
|
25
|
-
/* noop */
|
|
26
|
-
},
|
|
23
|
+
execute: noop,
|
|
27
24
|
},
|
|
28
25
|
};
|
|
29
26
|
const schemas = agentToolsToSchemas(tools);
|
package/sdk/_internal-types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
package/sdk/_test-matchers.ts
CHANGED
|
@@ -12,16 +12,24 @@ import { ClientEventSchema } from "./protocol.ts";
|
|
|
12
12
|
|
|
13
13
|
type MatcherResult = { pass: boolean; message: () => string };
|
|
14
14
|
|
|
15
|
-
|
|
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:
|
|
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
|
|
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:
|
|
63
|
+
pass: false,
|
|
48
64
|
message: () =>
|
|
49
|
-
|
|
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(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
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(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
});
|
package/sdk/allowed-hosts.ts
CHANGED
|
@@ -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
|
|
16
|
+
type ValidationFailure = { valid: false; reason: string };
|
|
17
|
+
type ValidationResult = { valid: true } | ValidationFailure;
|
|
22
18
|
|
|
23
|
-
function fail(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
|
|
90
|
+
// Strip port only when there are no brackets (preserves IPv6 bracket notation).
|
|
95
91
|
const portIndex = hostname.lastIndexOf(":");
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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
|
-
*
|
|
77
|
-
*
|
|
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:; " +
|