mirror of
https://github.com/openai/codex.git
synced 2026-03-12 00:55:39 +03:00
Compare commits
4 Commits
main
...
keyz/codeg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0e68a98b | ||
|
|
49986a1679 | ||
|
|
69f6adec3b | ||
|
|
56b60a1790 |
@@ -56,7 +56,7 @@ fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
);
|
||||
section.push_str("- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n");
|
||||
section.push_str(
|
||||
"- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n",
|
||||
"- Helpers: `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n",
|
||||
);
|
||||
section.push_str("- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n");
|
||||
section.push_str("- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n");
|
||||
@@ -498,7 +498,7 @@ mod tests {
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
@@ -517,7 +517,7 @@ mod tests {
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Do not call tools directly; use `js_repl` + `codex.tool(...)` for all tool calls, including shell commands.\n- MCP tools (if any) can also be called by name via `codex.tool(...)`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Do not call tools directly; use `js_repl` + `codex.tool(...)` for all tool calls, including shell commands.\n- MCP tools (if any) can also be called by name via `codex.tool(...)`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
@@ -536,7 +536,7 @@ mod tests {
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,9 +127,7 @@ const pendingTool = new Map();
|
||||
const pendingEmitImage = new Map();
|
||||
let toolCounter = 0;
|
||||
let emitImageCounter = 0;
|
||||
const cwd = process.cwd();
|
||||
const tmpDir = process.env.CODEX_JS_TMP_DIR || cwd;
|
||||
const homeDir = process.env.HOME ?? null;
|
||||
const tmpDir = process.env.CODEX_JS_TMP_DIR || process.cwd();
|
||||
const nodeModuleDirEnv = process.env.CODEX_JS_REPL_NODE_MODULE_DIRS ?? "";
|
||||
const moduleSearchBases = (() => {
|
||||
const bases = [];
|
||||
@@ -152,6 +150,7 @@ const moduleSearchBases = (() => {
|
||||
seen.add(base);
|
||||
bases.push(base);
|
||||
}
|
||||
const cwd = process.cwd();
|
||||
if (!seen.has(cwd)) {
|
||||
bases.push(cwd);
|
||||
}
|
||||
@@ -1540,12 +1539,12 @@ async function handleExec(message) {
|
||||
priorBindings = builtSource.priorBindings;
|
||||
let output = "";
|
||||
|
||||
context.codex = { cwd, homeDir, tmpDir, tool, emitImage };
|
||||
context.codex = { tmpDir, tool, emitImage };
|
||||
context.tmpDir = tmpDir;
|
||||
|
||||
await withCapturedConsole(context, async (logs) => {
|
||||
const cellIdentifier = path.join(
|
||||
cwd,
|
||||
process.cwd(),
|
||||
`.codex_js_repl_cell_${cellCounter++}.mjs`,
|
||||
);
|
||||
module = new SourceTextModule(source, {
|
||||
|
||||
@@ -678,13 +678,8 @@ impl JsReplManager {
|
||||
let (stdin, pending_execs, exec_contexts, child, recent_stderr) = {
|
||||
let mut kernel = self.kernel.lock().await;
|
||||
if kernel.is_none() {
|
||||
let dependency_env = session.dependency_env().await;
|
||||
let state = self
|
||||
.start_kernel(
|
||||
Arc::clone(&turn),
|
||||
&dependency_env,
|
||||
Some(session.conversation_id),
|
||||
)
|
||||
.start_kernel(Arc::clone(&turn), Some(session.conversation_id))
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
*kernel = Some(state);
|
||||
@@ -805,7 +800,6 @@ impl JsReplManager {
|
||||
async fn start_kernel(
|
||||
&self,
|
||||
turn: Arc<TurnContext>,
|
||||
dependency_env: &HashMap<String, String>,
|
||||
thread_id: Option<ThreadId>,
|
||||
) -> Result<KernelState, String> {
|
||||
let node_path = resolve_compatible_node(self.node_path.as_deref()).await?;
|
||||
@@ -816,9 +810,6 @@ impl JsReplManager {
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let mut env = create_env(&turn.shell_environment_policy, thread_id);
|
||||
if !dependency_env.is_empty() {
|
||||
env.extend(dependency_env.clone());
|
||||
}
|
||||
env.insert(
|
||||
"CODEX_JS_TMP_DIR".to_string(),
|
||||
self.tmp_dir.path().to_string_lossy().to_string(),
|
||||
@@ -3455,22 +3446,13 @@ await codex.emitImage(out);
|
||||
}
|
||||
|
||||
let cwd_dir = tempdir()?;
|
||||
let expected_home_dir = serde_json::to_string("/tmp/codex-home")?;
|
||||
write_js_repl_test_module(
|
||||
cwd_dir.path(),
|
||||
"globals.js",
|
||||
&format!(
|
||||
"const expectedHomeDir = {expected_home_dir};\nconsole.log(`tmp:${{codex.tmpDir === tmpDir}}`);\nconsole.log(`cwd:${{typeof codex.cwd}}:${{codex.cwd.length > 0}}`);\nconsole.log(`home:${{codex.homeDir === expectedHomeDir}}`);\nconsole.log(`tool:${{typeof codex.tool}}`);\nconsole.log(\"local-file-console-ok\");\n"
|
||||
),
|
||||
"console.log(codex.tmpDir === tmpDir);\nconsole.log(typeof codex.tool);\nconsole.log(\"local-file-console-ok\");\n",
|
||||
)?;
|
||||
|
||||
let (session, mut turn) = make_session_and_context().await;
|
||||
session
|
||||
.set_dependency_env(HashMap::from([(
|
||||
"HOME".to_string(),
|
||||
"/tmp/codex-home".to_string(),
|
||||
)]))
|
||||
.await;
|
||||
turn.shell_environment_policy
|
||||
.r#set
|
||||
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
|
||||
@@ -3496,10 +3478,8 @@ await codex.emitImage(out);
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
assert!(result.output.contains("tmp:true"));
|
||||
assert!(result.output.contains("cwd:string:true"));
|
||||
assert!(result.output.contains("home:true"));
|
||||
assert!(result.output.contains("tool:function"));
|
||||
assert!(result.output.contains("true"));
|
||||
assert!(result.output.contains("function"));
|
||||
assert!(result.output.contains("local-file-console-ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -659,34 +659,6 @@ async fn js_repl_does_not_expose_process_global() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_exposes_codex_path_helpers() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = run_js_repl_turn(
|
||||
&server,
|
||||
"check codex path helpers",
|
||||
&[(
|
||||
"call-1",
|
||||
"console.log(`cwd:${typeof codex.cwd}:${codex.cwd.length > 0}`); console.log(`home:${codex.homeDir === null || typeof codex.homeDir === \"string\"}`);",
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = mock.single_request();
|
||||
let (output, success) = custom_tool_output_text_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"js_repl call failed unexpectedly: {output}"
|
||||
);
|
||||
assert!(output.contains("cwd:string:true"));
|
||||
assert!(output.contains("home:true"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_blocks_sensitive_builtin_imports() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -30,7 +30,6 @@ use crate::upstream::UpstreamClient;
|
||||
use crate::upstream::proxy_for_connect;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
use rama_core::Layer;
|
||||
use rama_core::Service;
|
||||
use rama_core::error::BoxError;
|
||||
@@ -39,6 +38,7 @@ use rama_core::error::OpaqueError;
|
||||
use rama_core::extensions::ExtensionsMut;
|
||||
use rama_core::extensions::ExtensionsRef;
|
||||
use rama_core::layer::AddInputExtensionLayer;
|
||||
use rama_core::rt::Executor;
|
||||
use rama_core::service::service_fn;
|
||||
use rama_http::Body;
|
||||
use rama_http::HeaderMap;
|
||||
@@ -113,17 +113,11 @@ async fn run_http_proxy_with_listener(
|
||||
listener: TcpListener,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
) -> Result<()> {
|
||||
ensure_rustls_crypto_provider();
|
||||
|
||||
let addr = listener
|
||||
.local_addr()
|
||||
.context("read HTTP proxy listener local addr")?;
|
||||
|
||||
// This proxy listener only needs HTTP/1 proxy semantics. Using Rama's auto builder
|
||||
// forces every accepted socket through the HTTP version sniffing pre-read path before proxy
|
||||
// request parsing, which can stall some local clients on macOS before CONNECT/absolute-form
|
||||
// handling runs at all.
|
||||
let http_service = HttpServer::http1().service(
|
||||
let http_service = HttpServer::auto(Executor::new()).service(
|
||||
(
|
||||
UpgradeLayer::new(
|
||||
MethodMatcher::CONNECT,
|
||||
@@ -983,14 +977,7 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rama_http::Method;
|
||||
use rama_http::Request;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::TcpListener as StdTcpListener;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpListener as TokioTcpListener;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_connect_accept_blocks_in_limited_mode() {
|
||||
@@ -1037,65 +1024,6 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_proxy_listener_accepts_plain_http1_connect_requests() {
|
||||
let target_listener = TokioTcpListener::bind((Ipv4Addr::LOCALHOST, 0))
|
||||
.await
|
||||
.expect("target listener should bind");
|
||||
let target_addr = target_listener
|
||||
.local_addr()
|
||||
.expect("target listener should expose local addr");
|
||||
let target_task = tokio::spawn(async move {
|
||||
let (mut stream, _) = target_listener
|
||||
.accept()
|
||||
.await
|
||||
.expect("target listener should accept");
|
||||
let mut buf = [0_u8; 1];
|
||||
let _ = timeout(Duration::from_secs(1), stream.read(&mut buf)).await;
|
||||
});
|
||||
|
||||
let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["127.0.0.1".to_string()],
|
||||
allow_local_binding: true,
|
||||
..NetworkProxySettings::default()
|
||||
}));
|
||||
let listener =
|
||||
StdTcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("proxy listener should bind");
|
||||
let proxy_addr = listener
|
||||
.local_addr()
|
||||
.expect("proxy listener should expose local addr");
|
||||
let proxy_task = tokio::spawn(run_http_proxy_with_std_listener(state, listener, None));
|
||||
|
||||
let mut stream = tokio::net::TcpStream::connect(proxy_addr)
|
||||
.await
|
||||
.expect("client should connect to proxy");
|
||||
let request = format!(
|
||||
"CONNECT 127.0.0.1:{port} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n\r\n",
|
||||
port = target_addr.port()
|
||||
);
|
||||
stream
|
||||
.write_all(request.as_bytes())
|
||||
.await
|
||||
.expect("client should write CONNECT request");
|
||||
|
||||
let mut buf = [0_u8; 256];
|
||||
let bytes_read = timeout(Duration::from_secs(2), stream.read(&mut buf))
|
||||
.await
|
||||
.expect("proxy should respond before timeout")
|
||||
.expect("client should read proxy response");
|
||||
let response = String::from_utf8_lossy(&buf[..bytes_read]);
|
||||
assert!(
|
||||
response.starts_with("HTTP/1.1 200 OK\r\n"),
|
||||
"unexpected proxy response: {response:?}"
|
||||
);
|
||||
|
||||
drop(stream);
|
||||
proxy_task.abort();
|
||||
let _ = proxy_task.await;
|
||||
target_task.abort();
|
||||
let _ = target_task.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn http_plain_proxy_blocks_unix_socket_when_method_not_allowed() {
|
||||
let state = Arc::new(network_proxy_state_for_policy(
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::state::NetworkProxyState;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpListener as StdTcpListener;
|
||||
@@ -432,6 +433,8 @@ impl NetworkProxy {
|
||||
return Ok(NetworkProxyHandle::noop());
|
||||
}
|
||||
|
||||
ensure_rustls_crypto_provider();
|
||||
|
||||
if !unix_socket_permissions_supported() {
|
||||
warn!(
|
||||
"allowUnixSockets and dangerouslyAllowAllUnixSockets are macOS-only; requests will be rejected on this platform"
|
||||
|
||||
7066
codex-rs/protocol/generated/rollout-line.schema.json
Normal file
7066
codex-rs/protocol/generated/rollout-line.schema.json
Normal file
File diff suppressed because it is too large
Load Diff
1360
codex-rs/protocol/generated/rollout-line.schema.ts
Normal file
1360
codex-rs/protocol/generated/rollout-line.schema.ts
Normal file
File diff suppressed because it is too large
Load Diff
104
codex-rs/protocol/src/bin/codex-write-rollout-line-schema.rs
Normal file
104
codex-rs/protocol/src/bin/codex-write-rollout-line-schema.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use std::any::TypeId;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use ts_rs::TS;
|
||||
use ts_rs::TypeVisitor;
|
||||
|
||||
const GENERATED_TS_HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
|
||||
const TS_RS_NOTE: &str = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n";
|
||||
const JSON_SCHEMA_FILENAME: &str = "rollout-line.schema.json";
|
||||
const TYPESCRIPT_FILENAME: &str = "rollout-line.schema.ts";
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let out_dir = std::env::args_os()
|
||||
.nth(1)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("generated"));
|
||||
|
||||
write_rollout_line_artifacts(&out_dir)
|
||||
}
|
||||
|
||||
fn write_rollout_line_artifacts(out_dir: &Path) -> Result<(), Box<dyn Error>> {
|
||||
std::fs::create_dir_all(&out_dir)?;
|
||||
|
||||
let schema_path = out_dir.join(JSON_SCHEMA_FILENAME);
|
||||
let schema = schemars::schema_for!(RolloutLine);
|
||||
let schema_json = serde_json::to_vec_pretty(&schema)?;
|
||||
std::fs::write(&schema_path, schema_json)?;
|
||||
|
||||
let typescript_path = out_dir.join(TYPESCRIPT_FILENAME);
|
||||
let typescript = generate_typescript_bundle::<RolloutLine>()?;
|
||||
std::fs::write(&typescript_path, typescript)?;
|
||||
|
||||
println!("wrote {}", schema_path.display());
|
||||
println!("wrote {}", typescript_path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_typescript_bundle<T: TS + 'static + ?Sized>() -> Result<String, Box<dyn Error>> {
|
||||
let mut declarations = BTreeMap::new();
|
||||
let mut seen = HashSet::new();
|
||||
collect_typescript_declarations::<T>(&mut declarations, &mut seen)?;
|
||||
|
||||
let body = declarations
|
||||
.into_values()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n")
|
||||
.replace("\r\n", "\n")
|
||||
.replace('\r', "\n");
|
||||
Ok(format!("{GENERATED_TS_HEADER}{TS_RS_NOTE}\n{body}\n"))
|
||||
}
|
||||
|
||||
fn collect_typescript_declarations<T: TS + 'static + ?Sized>(
|
||||
declarations: &mut BTreeMap<PathBuf, String>,
|
||||
seen: &mut HashSet<TypeId>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let Some(output_path) = T::output_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
if !seen.insert(TypeId::of::<T>()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut declaration = String::new();
|
||||
if let Some(docs) = T::docs() {
|
||||
declaration.push_str(&docs.replace("\r\n", "\n").replace('\r', "\n"));
|
||||
}
|
||||
declaration.push_str("export ");
|
||||
declaration.push_str(&T::decl().replace("\r\n", "\n").replace('\r', "\n"));
|
||||
declarations.insert(output_path.components().collect(), declaration);
|
||||
|
||||
let mut visitor = TypeScriptDeclarationCollector {
|
||||
declarations,
|
||||
seen,
|
||||
error: None,
|
||||
};
|
||||
T::visit_dependencies(&mut visitor);
|
||||
if let Some(error) = visitor.error {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TypeScriptDeclarationCollector<'a> {
|
||||
declarations: &'a mut BTreeMap<PathBuf, String>,
|
||||
seen: &'a mut HashSet<TypeId>,
|
||||
error: Option<Box<dyn Error>>,
|
||||
}
|
||||
|
||||
impl TypeVisitor for TypeScriptDeclarationCollector<'_> {
|
||||
fn visit<T: TS + 'static + ?Sized>(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.error = collect_typescript_declarations::<T>(self.declarations, self.seen).err();
|
||||
}
|
||||
}
|
||||
@@ -2423,7 +2423,7 @@ pub enum TruncationPolicy {
|
||||
Tokens(usize),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, JsonSchema, TS)]
|
||||
pub struct RolloutLine {
|
||||
pub timestamp: String,
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -3686,18 +3686,10 @@ impl App {
|
||||
}
|
||||
|
||||
async fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) {
|
||||
// Some terminals, especially on macOS, encode Option+Left/Right as Option+b/f unless
|
||||
// enhanced keyboard reporting is available. We only treat those word-motion fallbacks as
|
||||
// agent-switch shortcuts when the composer is empty so we never steal the expected
|
||||
// editing behavior for moving across words inside a draft.
|
||||
let allow_agent_word_motion_fallback = !self.enhanced_keys_supported
|
||||
&& self.chat_widget.composer_text_with_pending().is_empty();
|
||||
if self.overlay.is_none()
|
||||
&& self.chat_widget.no_modal_or_popup_active()
|
||||
// Alt+Left/Right are also natural word-motion keys in the composer. Keep agent
|
||||
// fast-switch available only once the draft is empty so editing behavior wins whenever
|
||||
// there is text on screen.
|
||||
&& self.chat_widget.composer_text_with_pending().is_empty()
|
||||
&& previous_agent_shortcut_matches(key_event, allow_agent_word_motion_fallback)
|
||||
{
|
||||
if let Some(thread_id) = self.agent_navigation.adjacent_thread_id(
|
||||
@@ -3710,9 +3702,6 @@ impl App {
|
||||
}
|
||||
if self.overlay.is_none()
|
||||
&& self.chat_widget.no_modal_or_popup_active()
|
||||
// Mirror the previous-agent rule above: empty drafts may use these keys for thread
|
||||
// switching, but non-empty drafts keep them for expected word-wise cursor motion.
|
||||
&& self.chat_widget.composer_text_with_pending().is_empty()
|
||||
&& next_agent_shortcut_matches(key_event, allow_agent_word_motion_fallback)
|
||||
{
|
||||
if let Some(thread_id) = self.agent_navigation.adjacent_thread_id(
|
||||
|
||||
@@ -121,10 +121,8 @@ fn previous_agent_word_motion_fallback(
|
||||
key_event: KeyEvent,
|
||||
allow_word_motion_fallback: bool,
|
||||
) -> bool {
|
||||
// Some terminals, especially on macOS, send Option+b/f as word-motion keys instead of
|
||||
// Option+arrow events unless enhanced keyboard reporting is enabled. Callers should only
|
||||
// enable this fallback when the composer is empty so draft editing retains the expected
|
||||
// word-wise motion behavior.
|
||||
// macOS terminals often send Option+b/f as word-motion keys instead of Option+arrow events
|
||||
// unless enhanced keyboard reporting is enabled.
|
||||
allow_word_motion_fallback
|
||||
&& matches!(
|
||||
key_event,
|
||||
@@ -147,10 +145,8 @@ fn previous_agent_word_motion_fallback(
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn next_agent_word_motion_fallback(key_event: KeyEvent, allow_word_motion_fallback: bool) -> bool {
|
||||
// Some terminals, especially on macOS, send Option+b/f as word-motion keys instead of
|
||||
// Option+arrow events unless enhanced keyboard reporting is enabled. Callers should only
|
||||
// enable this fallback when the composer is empty so draft editing retains the expected
|
||||
// word-wise motion behavior.
|
||||
// macOS terminals often send Option+b/f as word-motion keys instead of Option+arrow events
|
||||
// unless enhanced keyboard reporting is enabled.
|
||||
allow_word_motion_fallback
|
||||
&& matches!(
|
||||
key_event,
|
||||
@@ -675,15 +671,7 @@ mod tests {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn agent_shortcut_matches_option_arrow_word_motion_fallbacks_only_when_allowed() {
|
||||
assert!(previous_agent_shortcut_matches(
|
||||
KeyEvent::new(KeyCode::Left, KeyModifiers::ALT),
|
||||
false,
|
||||
));
|
||||
assert!(next_agent_shortcut_matches(
|
||||
KeyEvent::new(KeyCode::Right, KeyModifiers::ALT),
|
||||
false,
|
||||
));
|
||||
fn agent_shortcut_matches_option_arrow_word_motion_fallbacks() {
|
||||
assert!(previous_agent_shortcut_matches(
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT),
|
||||
true,
|
||||
@@ -702,27 +690,6 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn agent_shortcut_matches_option_arrows_only() {
|
||||
assert!(previous_agent_shortcut_matches(
|
||||
KeyEvent::new(KeyCode::Left, crossterm::event::KeyModifiers::ALT,),
|
||||
false
|
||||
));
|
||||
assert!(next_agent_shortcut_matches(
|
||||
KeyEvent::new(KeyCode::Right, crossterm::event::KeyModifiers::ALT,),
|
||||
false
|
||||
));
|
||||
assert!(!previous_agent_shortcut_matches(
|
||||
KeyEvent::new(KeyCode::Char('b'), crossterm::event::KeyModifiers::ALT,),
|
||||
false
|
||||
));
|
||||
assert!(!next_agent_shortcut_matches(
|
||||
KeyEvent::new(KeyCode::Char('f'), crossterm::event::KeyModifiers::ALT,),
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_styles_nickname_and_role() {
|
||||
let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")
|
||||
|
||||
@@ -74,8 +74,6 @@ imported local file. They are not resolved relative to the imported file's locat
|
||||
|
||||
`js_repl` exposes these globals:
|
||||
|
||||
- `codex.cwd`: REPL working directory path.
|
||||
- `codex.homeDir`: effective home directory path from the kernel environment.
|
||||
- `codex.tmpDir`: per-session scratch directory path.
|
||||
- `codex.tool(name, args?)`: executes a normal Codex tool call from inside `js_repl` (including shell tools like `shell` / `shell_command` when available).
|
||||
- `codex.emitImage(imageLike)`: explicitly adds one image to the outer `js_repl` function output each time you call it.
|
||||
|
||||
Reference in New Issue
Block a user