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