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