1use crate::{ConnectorDescription, generate_types, normalize_code};
2
3#[derive(Debug, Clone)]
5pub struct ProgramSourceOptions {
6 pub dispatch: String,
8 pub execution_id: String,
10 pub timeout_ms: Option<u64>,
12}
13
14pub fn build_program_source(
16 code: &str,
17 connectors: &[ConnectorDescription],
18 options: &ProgramSourceOptions,
19) -> Result<String, String> {
20 validate_names(connectors)?;
21 let code = normalize_code(code);
22 let bindings = connectors
23 .iter()
24 .map(|connector| {
25 let methods = connector
26 .tools
27 .iter()
28 .map(|tool| {
29 format!(
30 "{}: async (args = {{}}) => __unwrap(await __dispatch({{ kind: 'call', seq: __seq++, connector: {}, method: {}, arguments: __encode(args) }}))",
31 serde_json::to_string(&tool.name).unwrap(),
32 serde_json::to_string(&connector.name).unwrap(),
33 serde_json::to_string(&tool.name).unwrap(),
34 )
35 })
36 .collect::<Vec<_>>()
37 .join(",\n");
38 format!("const {} = {{\n{methods}\n}};", connector.name)
39 })
40 .collect::<Vec<_>>()
41 .join("\n");
42 let type_comment = connectors
43 .iter()
44 .map(generate_types)
45 .collect::<Vec<_>>()
46 .join("\n\n")
47 .replace("*/", "*\\/");
48 let run = if let Some(timeout_ms) = options.timeout_ms {
49 format!(
50 r#"await Promise.race([
51 __program(),
52 new Promise((_, reject) => setTimeout(() => reject(new Error("Code execution timed out after {timeout_ms}ms")), {timeout_ms}))
53 ])"#
54 )
55 } else {
56 "await __program()".to_string()
57 };
58 Ok(format!(
59 r#"/* Model-facing declarations:
60{type_comment}
61*/
62const __logs = [];
63let __seq = 0;
64const console = {{
65 log: (...values) => __logs.push(values.map(String).join(" ")),
66 info: (...values) => __logs.push(values.map(String).join(" ")),
67 warn: (...values) => __logs.push(values.map(String).join(" ")),
68 error: (...values) => __logs.push(values.map(String).join(" "))
69}};
70const __base64Encode = (value) => {{
71 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
72 let result = "";
73 for (let index = 0; index < value.length; index += 3) {{
74 const first = value[index];
75 const second = value[index + 1];
76 const third = value[index + 2];
77 result += alphabet[first >> 2];
78 result += alphabet[((first & 3) << 4) | ((second ?? 0) >> 4)];
79 result += second === undefined ? "=" : alphabet[((second & 15) << 2) | ((third ?? 0) >> 6)];
80 result += third === undefined ? "=" : alphabet[third & 63];
81 }}
82 return result;
83}};
84const __base64Decode = (value) => {{
85 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
86 const clean = value.replace(/=+$/, "");
87 const bytes = [];
88 let buffer = 0;
89 let bits = 0;
90 for (const char of clean) {{
91 buffer = (buffer << 6) | alphabet.indexOf(char);
92 bits += 6;
93 if (bits >= 8) {{
94 bits -= 8;
95 bytes.push((buffer >> bits) & 255);
96 }}
97 }}
98 return new Uint8Array(bytes);
99}};
100const __encode = (value) => {{
101 if (value === undefined) return {{ __codemode_type: "undefined" }};
102 if (typeof value === "bigint") return {{ __codemode_type: "bigint", value: value.toString() }};
103 if (value instanceof Uint8Array) return {{ __codemode_type: "binary", value: __base64Encode(value) }};
104 if (Array.isArray(value)) return value.map(__encode);
105 if (value && typeof value === "object") {{
106 return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, __encode(child)]));
107 }}
108 return value;
109}};
110const __decode = (value) => {{
111 if (Array.isArray(value)) return value.map(__decode);
112 if (value && typeof value === "object") {{
113 if (value.__codemode_type === "undefined") return undefined;
114 if (value.__codemode_type === "bigint") return BigInt(value.value);
115 if (value.__codemode_type === "binary") return __base64Decode(value.value);
116 return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, __decode(child)]));
117 }}
118 return value;
119}};
120const __dispatch = {dispatch};
121const __unwrap = (response) => {{
122 if (response.__codemode_control__ === "pause") throw new Error("__CODEMODE_PAUSE__");
123 if (response.__codemode_control__ === "error") throw new Error(response.message);
124 return __decode(response.result);
125}};
126{bindings}
127const __callBuiltin = async (method, args) => __unwrap(await __dispatch({{
128 kind: "call",
129 seq: __seq++,
130 connector: "codemode",
131 method,
132 arguments: __encode(args)
133}}));
134const codemode = {{
135 executionId: {execution_id},
136 search: (query) => __callBuiltin("search", {{ query }}),
137 describe: (target) => __callBuiltin("describe", {{ target }}),
138 step: async (name, fn) => {{
139 const seq = __seq++;
140 const decision = await __dispatch({{ kind: "begin_step", seq, name }});
141 if (decision.kind === "pause") throw new Error("__CODEMODE_PAUSE__");
142 if (decision.kind === "replay") return __decode(decision.result);
143 const result = await fn();
144 await __dispatch({{ kind: "record_step", seq, result: __encode(result) }});
145 return result;
146 }}
147}};
148try {{
149 const __program = ({code});
150 const __result = {run};
151 return {{ result: __encode(__result), error: null, logs: __logs }};
152}} catch (error) {{
153 return {{
154 result: null,
155 error: error instanceof Error ? error.message : String(error),
156 logs: __logs
157 }};
158}}"#,
159 dispatch = options.dispatch,
160 execution_id = options.execution_id,
161 ))
162}
163
164pub(crate) const RESERVED_CONNECTOR_NAMES: &[&str] = &[
171 "__incursDispatch",
173 "__dispatch",
174 "__encode",
175 "__decode",
176 "__unwrap",
177 "__seq",
178 "__logs",
179 "__program",
180 "__result",
181 "__base64Encode",
182 "__base64Decode",
183 "__callBuiltin",
184 "codemode",
185 "console",
186 "Promise",
188 "setTimeout",
189 "Error",
190 "fetch",
191 "JSON",
192 "Object",
193 "Array",
194 "String",
195 "Number",
196 "Boolean",
197 "BigInt",
198 "Uint8Array",
199 "Math",
200 "Symbol",
201 "globalThis",
202 "await",
205 "break",
206 "case",
207 "catch",
208 "class",
209 "const",
210 "continue",
211 "debugger",
212 "default",
213 "delete",
214 "do",
215 "else",
216 "enum",
217 "export",
218 "extends",
219 "false",
220 "finally",
221 "for",
222 "function",
223 "if",
224 "implements",
225 "import",
226 "in",
227 "instanceof",
228 "interface",
229 "let",
230 "new",
231 "null",
232 "package",
233 "private",
234 "protected",
235 "public",
236 "return",
237 "static",
238 "super",
239 "switch",
240 "this",
241 "throw",
242 "true",
243 "try",
244 "typeof",
245 "var",
246 "void",
247 "while",
248 "with",
249 "yield",
250];
251
252fn validate_names(connectors: &[ConnectorDescription]) -> Result<(), String> {
253 let mut seen = std::collections::BTreeSet::new();
254 for connector in connectors {
255 if RESERVED_CONNECTOR_NAMES.contains(&connector.name.as_str()) {
256 return Err(format!("Connector name \"{}\" is reserved", connector.name));
257 }
258 if !valid_identifier(&connector.name) {
259 return Err(format!(
260 "Connector name \"{}\" is not a valid JavaScript identifier",
261 connector.name
262 ));
263 }
264 if !seen.insert(&connector.name) {
265 return Err(format!("Duplicate connector name \"{}\"", connector.name));
266 }
267 let mut methods = std::collections::BTreeSet::new();
268 for tool in &connector.tools {
269 if !methods.insert(&tool.name) {
270 return Err(format!(
271 "Duplicate tool name \"{}\" in connector \"{}\"",
272 tool.name, connector.name
273 ));
274 }
275 }
276 }
277 Ok(())
278}
279
280fn valid_identifier(value: &str) -> bool {
281 let mut chars = value.chars();
282 chars
283 .next()
284 .is_some_and(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphabetic())
285 && chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
286}
287
288#[cfg(test)]
289mod tests {
290 use serde_json::json;
291
292 use crate::{ConnectorTool, ToolAnnotations};
293
294 use super::*;
295
296 #[test]
297 fn emits_shared_connector_and_step_harness() {
298 let source = build_program_source(
299 "state.read({ id: 1 })",
300 &[ConnectorDescription {
301 name: "state".to_string(),
302 instructions: None,
303 tools: vec![ConnectorTool {
304 name: "read".to_string(),
305 description: None,
306 input_schema: json!({"type": "object"}),
307 output_schema: None,
308 instructions: None,
309 examples: Vec::new(),
310 annotations: ToolAnnotations::default(),
311 policy: crate::ToolPolicy::default(),
312 }],
313 }],
314 &ProgramSourceOptions {
315 dispatch: "async (payload) => payload".to_string(),
316 execution_id: "\"test\"".to_string(),
317 timeout_ms: None,
318 },
319 )
320 .unwrap();
321
322 assert!(source.contains("const state"));
323 assert!(source.contains("const __dispatch = async (payload) => payload"));
324 assert!(source.contains("step: async (name, fn)"));
325 }
326
327 fn named(name: &str) -> ConnectorDescription {
328 ConnectorDescription {
329 name: name.to_string(),
330 instructions: None,
331 tools: vec![ConnectorTool {
332 name: "read".to_string(),
333 description: None,
334 input_schema: json!({"type": "object"}),
335 output_schema: None,
336 instructions: None,
337 examples: Vec::new(),
338 annotations: ToolAnnotations::default(),
339 policy: crate::ToolPolicy::default(),
340 }],
341 }
342 }
343
344 fn build(name: &str) -> Result<String, String> {
345 build_program_source(
346 "return 1",
347 &[named(name)],
348 &ProgramSourceOptions {
349 dispatch: "async (payload) => payload".to_string(),
350 execution_id: "\"test\"".to_string(),
351 timeout_ms: None,
352 },
353 )
354 }
355
356 #[test]
357 fn rejects_names_the_harness_already_binds() {
358 for name in ["__callBuiltin", "__base64Encode", "__base64Decode"] {
361 assert!(
362 build(name).is_err(),
363 "{name} is bound by the harness but was accepted"
364 );
365 }
366 }
367
368 #[test]
369 fn rejects_reserved_words_that_cannot_be_bound() {
370 for name in ["class", "default", "delete", "new", "import", "return"] {
373 assert!(
374 build(name).is_err(),
375 "reserved word {name} was accepted as a connector name"
376 );
377 }
378 }
379
380 #[test]
381 fn accepts_an_ordinary_name_that_merely_resembles_a_reserved_one() {
382 for name in ["mcp_fetch", "classroom", "defaults", "newRelic"] {
384 assert!(build(name).is_ok(), "{name} should be a usable namespace");
385 }
386 }
387}