Skip to main content

incurs_codemode/
program.rs

1use crate::{ConnectorDescription, generate_types, normalize_code};
2
3/// Adapter-specific settings for the shared Code Mode JavaScript harness.
4#[derive(Debug, Clone)]
5pub struct ProgramSourceOptions {
6    /// JavaScript expression that evaluates to the asynchronous dispatch function.
7    pub dispatch: String,
8    /// JavaScript expression exposed as `codemode.executionId`.
9    pub execution_id: String,
10    /// Optional JavaScript timer timeout for hosts that provide `setTimeout`.
11    pub timeout_ms: Option<u64>,
12}
13
14/// Builds the shared body used by local and remote Code Mode executors.
15pub 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
164/// Names the generated program already binds, or that JavaScript refuses as a
165/// binding.
166///
167/// A connector taking one of these produces a `const` redeclaration or a parse
168/// error inside the sandbox, which surfaces far from its cause, so it is
169/// rejected here where the name is still attributable to a connector.
170pub(crate) const RESERVED_CONNECTOR_NAMES: &[&str] = &[
171    // Bindings emitted by `build_program_source`, in declaration order.
172    "__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    // Globals the harness itself calls.
187    "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    // Reserved words. `valid_identifier` accepts these, but `const <word> = …`
203    // is a syntax error.
204    "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        // These are `const`-declared by `build_program_source` itself, so a
359        // connector taking one produces a redeclaration inside the sandbox.
360        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        // `valid_identifier` accepts each of these, but `const class = {…}` is
371        // a syntax error that would surface far from the offending connector.
372        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        // The guard is exact-match, so renaming around a collision works.
383        for name in ["mcp_fetch", "classroom", "defaults", "newRelic"] {
384            assert!(build(name).is_ok(), "{name} should be a usable namespace");
385        }
386    }
387}