mirror of
https://github.com/openai/codex.git
synced 2026-05-09 13:52:41 +00:00
Compare commits
20 Commits
fcoury/mar
...
dev/jlewi/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78cdff9465 | ||
|
|
fe2ff29bef | ||
|
|
64f78a3dc5 | ||
|
|
45cfcb0c7a | ||
|
|
b6cc089db5 | ||
|
|
2b362f9c51 | ||
|
|
c97c42d317 | ||
|
|
60a80828e7 | ||
|
|
a050c5ee91 | ||
|
|
dfbd21e294 | ||
|
|
e8cdf68bdd | ||
|
|
54d765635a | ||
|
|
11e16ef2e8 | ||
|
|
bb02604501 | ||
|
|
609b6168b3 | ||
|
|
5bc4c5c997 | ||
|
|
ee4138c363 | ||
|
|
3ffd99cd3d | ||
|
|
fa5d14a81f | ||
|
|
032b8dddb6 |
309
20260414_wasm_support.md
Normal file
309
20260414_wasm_support.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# WASM Support In Codex
|
||||
|
||||
## Purpose
|
||||
|
||||
This document summarizes the design and implementation changes needed to support running the Codex agent harness in a `wasm32` browser environment.
|
||||
|
||||
The goals are:
|
||||
|
||||
1. Identify the points in the Codex harness that need to be refactored to support WASM so we can discuss the right upstream abstractions with Codex maintainers.
|
||||
2. Record the major categories of changes required by the current prototype so that future rebases can distinguish essential seams from incidental implementation details.
|
||||
|
||||
This document is not intended to be a complete walkthrough of every file changed in the prototype branch. It is intended to capture the architectural pressure points.
|
||||
|
||||
## Non-Goal
|
||||
|
||||
This document does not argue that the browser should support the full native Codex feature set on day one. The prototype relies on degraded mode in several places. The main question is whether the Codex orchestration loop can run in the browser with a reduced environment surface. The prototype shows that it can.
|
||||
|
||||
## Key Result From The Prototype
|
||||
|
||||
The main Codex turn loop did not require a fundamental redesign to run in the browser.
|
||||
|
||||
The browser demo now runs a real turn through the Codex harness by creating a real thread and calling:
|
||||
|
||||
```rust
|
||||
session.thread.submit(Op::UserTurn { ... }).await
|
||||
```
|
||||
|
||||
The necessary refactoring was mostly at the environment boundary:
|
||||
|
||||
- networking and streaming
|
||||
- async runtime semantics
|
||||
- code execution runtime
|
||||
- filesystem/config/state assumptions
|
||||
- tool inventory assembly
|
||||
- native-only utility crates
|
||||
|
||||
The orchestrator was largely reusable. The host-heavy edges were the real blockers.
|
||||
|
||||
## High-Level Framing
|
||||
|
||||
The useful architectural question is:
|
||||
|
||||
> What assumptions does the Codex harness make about the runtime environment, and which of those assumptions must become explicit abstractions or degraded-mode implementations for `wasm32`?
|
||||
|
||||
In practice, the current prototype required two kinds of changes:
|
||||
|
||||
1. Compile-time separation of native and wasm implementations.
|
||||
2. Runtime degradation for features that are not required to complete a basic turn in the browser.
|
||||
|
||||
## Refactor Points Required For WASM
|
||||
|
||||
### 1. Responses API Transport And Streaming
|
||||
|
||||
This is one of the core seams.
|
||||
|
||||
The Codex harness assumes streamed model events. The browser prototype preserved that shape rather than introducing a separate unary or mock model path.
|
||||
|
||||
What needed refactoring:
|
||||
|
||||
- the HTTP transport needed a wasm-compatible implementation
|
||||
- SSE parsing and stream handling needed to work in wasm
|
||||
- native-only transport details needed to be compile-gated
|
||||
|
||||
Relevant files from the prototype:
|
||||
|
||||
- `codex-rs/codex-client/src/transport.rs`
|
||||
- `codex-rs/codex-client/src/sse.rs`
|
||||
- `codex-rs/codex-api/src/sse/responses.rs`
|
||||
- `codex-rs/codex-client/src/custom_ca_wasm.rs`
|
||||
|
||||
Upstream discussion point:
|
||||
|
||||
- There should be a stable transport boundary for Responses HTTP plus SSE.
|
||||
- Websocket support can remain native-only at first if it is cleanly compiled out on wasm.
|
||||
|
||||
### 2. Async Runtime Semantics
|
||||
|
||||
WASM support did not require removing async Rust. It did require making native assumptions about `Send` and task spawning explicit.
|
||||
|
||||
The key issue is that native Tokio code often assumes futures can safely satisfy `Send`, while browser-backed values may not.
|
||||
|
||||
What needed refactoring:
|
||||
|
||||
- wasm-specific async surfaces where `?Send` is required
|
||||
- compatibility wrappers for spawn/timing behavior
|
||||
- preserving native `Send` guarantees instead of weakening them globally
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `codex-rs/core/src/async_runtime.rs`
|
||||
- `codex-rs/async-utils/src/lib.rs`
|
||||
|
||||
Prototype lesson:
|
||||
|
||||
- This is a real source of regressions when doing a wasm split.
|
||||
- The correct pattern is usually platform-specific impls or conditional traits, not silently weakening native guarantees.
|
||||
|
||||
Upstream discussion point:
|
||||
|
||||
- A small async compatibility layer is a better seam than scattering `cfg(target_arch = "wasm32")` checks throughout orchestration code.
|
||||
|
||||
### 3. Code Execution Runtime
|
||||
|
||||
This is the most important architectural seam for browser execution.
|
||||
|
||||
The browser does not need native shell execution to run a useful Codex turn. It does need a model-visible execution surface for code mode.
|
||||
|
||||
The prototype solved this by injecting a browser-specific code-mode runtime backed by an iframe sandbox.
|
||||
|
||||
What needed refactoring:
|
||||
|
||||
- thread/session startup needed a way to accept a non-native code-mode runtime
|
||||
- code mode needed to remain enabled on wasm
|
||||
- nested native tools needed to be absent or reduced in wasm mode
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `codex-rs/wasm-harness/src/browser.rs`
|
||||
- `codex-rs/core/src/thread_manager.rs`
|
||||
- `codex-rs/core/src/codex.rs`
|
||||
- `codex-rs/core/src/tools/code_mode/mod.rs`
|
||||
- `codex-rs/code-mode/src/description.rs`
|
||||
|
||||
Prototype lesson:
|
||||
|
||||
- The orchestrator can stay the same if code execution is treated as an injected environment capability.
|
||||
|
||||
Upstream discussion point:
|
||||
|
||||
- The code-mode runtime boundary should be treated as a first-class extension point.
|
||||
- Browser execution can then become one implementation, not a fork of the harness loop.
|
||||
|
||||
### 4. Filesystem, Config, Memory, And Session State
|
||||
|
||||
Codex currently assumes a host filesystem in many places:
|
||||
|
||||
- config loading
|
||||
- project docs and instruction files
|
||||
- memory artifacts
|
||||
- rollout persistence
|
||||
- message history
|
||||
- parts of the state bridge
|
||||
|
||||
For the browser prototype, most of these did not need full browser implementations. They needed degraded-mode behavior so a basic turn could complete.
|
||||
|
||||
What needed refactoring:
|
||||
|
||||
- local file reads needed wasm-safe or stubbed alternatives
|
||||
- config loading needed a wasm-aware path
|
||||
- memory and history handling needed ephemeral or no-op behavior
|
||||
- state persistence needed to tolerate the absence of the native DB/runtime
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `codex-rs/core/src/async_fs.rs`
|
||||
- `codex-rs/core/src/config_loader_wasm.rs`
|
||||
- `codex-rs/core/src/memories_wasm.rs`
|
||||
- `codex-rs/core/src/message_history_wasm.rs`
|
||||
- `codex-rs/core/src/state_db_bridge.rs`
|
||||
- `codex-rs/core/src/project_doc_wasm.rs`
|
||||
|
||||
Prototype lesson:
|
||||
|
||||
- For a browser v0, the important distinction is between in-turn required state and optional persisted state.
|
||||
- Many of these systems can run in degraded mode initially.
|
||||
|
||||
Upstream discussion point:
|
||||
|
||||
- The harness should make instruction/config/state sources injectable where practical.
|
||||
- Optional persistence should be cleanly separable from turn execution.
|
||||
|
||||
### 5. Tool Inventory Assembly
|
||||
|
||||
The browser does not need every built-in Codex tool to complete a basic turn.
|
||||
|
||||
The important requirement is that the harness can assemble a reduced, coherent tool surface from available capabilities.
|
||||
|
||||
What needed refactoring:
|
||||
|
||||
- built-in tool inventory logic had to tolerate absent native capabilities
|
||||
- code-mode-only browser execution had to work with fewer nested tools
|
||||
- native-only tools had to be omitted rather than assumed
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `codex-rs/core/src/tools/spec.rs`
|
||||
- `codex-rs/core/src/tools/handlers/mod.rs`
|
||||
- `codex-rs/core/src/tools/code_mode/mod.rs`
|
||||
|
||||
Prototype lesson:
|
||||
|
||||
- Browser support does not require porting every native tool.
|
||||
- It requires that tools be derived from capabilities rather than hard-wired native assumptions.
|
||||
|
||||
Upstream discussion point:
|
||||
|
||||
- Tool assembly should be capability-driven.
|
||||
- That would align well with the broader orchestrator/environment split.
|
||||
|
||||
### 6. Native-Only Utility Crates
|
||||
|
||||
Several crates assumed native behavior directly at crate root, which prevented wasm compilation even when the browser did not need their full behavior.
|
||||
|
||||
The main fix was to split these crates into native/wasm implementations and re-export through a common surface.
|
||||
|
||||
Examples:
|
||||
|
||||
- `codex-rs/apply-patch/src/lib.rs`
|
||||
- `codex-rs/apply-patch/src/native.rs`
|
||||
- `codex-rs/apply-patch/src/wasm.rs`
|
||||
- `codex-rs/secrets/src/lib.rs`
|
||||
- `codex-rs/secrets/src/native.rs`
|
||||
- `codex-rs/secrets/src/wasm.rs`
|
||||
- `codex-rs/login/src/wasm.rs`
|
||||
- `codex-rs/utils/pty/src/process_wasm.rs`
|
||||
|
||||
Prototype lesson:
|
||||
|
||||
- Some of these crates are not conceptually part of the wasm browser runtime.
|
||||
- They still need compile-time separation so the orchestrator can build.
|
||||
|
||||
Upstream discussion point:
|
||||
|
||||
- Native/wasm crate structure should minimize leakage of host assumptions into shared orchestration code.
|
||||
|
||||
### 7. Native Execution, Sandboxing, And Process APIs
|
||||
|
||||
Native Codex includes shell execution, PTY handling, sandbox policies, JS REPL internals, and related runtime services that do not map directly to browser execution.
|
||||
|
||||
For the browser prototype, the correct move was not to emulate the full native stack. The correct move was to provide wasm-specific modules that let the core crate compile and let the browser run with a reduced execution surface.
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `codex-rs/core/src/exec_wasm.rs`
|
||||
- `codex-rs/core/src/exec_policy_wasm.rs`
|
||||
- `codex-rs/core/src/landlock_wasm.rs`
|
||||
- `codex-rs/core/src/tools/js_repl_wasm.rs`
|
||||
- `codex-rs/core/src/mcp_connection_manager_wasm.rs`
|
||||
|
||||
Upstream discussion point:
|
||||
|
||||
- Executor and sandboxing concerns should remain environment-owned.
|
||||
- The orchestrator should depend on explicit capability surfaces, not directly on native process behavior.
|
||||
|
||||
## What Did Not Need Fundamental Refactoring
|
||||
|
||||
The following was the most important finding from the prototype:
|
||||
|
||||
- the main turn loop did not need to be replaced
|
||||
- the browser did not need a custom harness loop
|
||||
- the browser did not need a fake event model
|
||||
|
||||
Once the host-heavy assumptions were refactored or degraded, the prototype could run a real turn through the existing Codex orchestration path.
|
||||
|
||||
This is important for upstream discussion because it suggests the right investment is not a second browser-specific harness. The right investment is making the existing harness more environment-aware.
|
||||
|
||||
## Rebase Guidance
|
||||
|
||||
Codex upstream is evolving quickly, including active work around orchestrator/environment boundaries. Future rebases are likely to encounter churn in exactly the areas touched by this prototype.
|
||||
|
||||
To make rebases tractable, it is useful to classify changes into two buckets.
|
||||
|
||||
### Essential Changes To Preserve
|
||||
|
||||
These reflect real architectural seams needed for wasm:
|
||||
|
||||
- Responses transport/SSE support
|
||||
- async runtime compatibility
|
||||
- injectable code execution runtime
|
||||
- degraded-mode config/state/memory handling
|
||||
- capability-driven tool assembly
|
||||
- native/wasm crate separation for host-bound utilities
|
||||
|
||||
### Changes That May Need Rework During Rebase
|
||||
|
||||
These are more implementation-specific and may not survive upstream changes unchanged:
|
||||
|
||||
- exact wasm stub file layout
|
||||
- exact browser sandbox message shape
|
||||
- exact prompt tuning in code mode
|
||||
- exact conditional compilation points
|
||||
- exact degraded-mode behavior for optional systems
|
||||
|
||||
The important rule for rebasing is:
|
||||
|
||||
> preserve the seam, not necessarily the exact patch.
|
||||
|
||||
## Questions To Discuss With Upstream Maintainers
|
||||
|
||||
1. What is the intended stable boundary for environment-specific execution capabilities?
|
||||
2. Should Responses networking be abstracted more explicitly, or is compile-gated transport injection sufficient?
|
||||
3. What is the intended abstraction boundary for code mode runtime injection?
|
||||
4. Which persistence systems are truly required for a turn, and which can be optional?
|
||||
5. Should tool inventory assembly become explicitly capability-driven?
|
||||
6. Which native-only crates should expose formal wasm stubs versus being moved behind higher-level interfaces?
|
||||
|
||||
## Practical Takeaway
|
||||
|
||||
The prototype suggests that supporting wasm is feasible without replacing the Codex agent harness.
|
||||
|
||||
The main work is to make runtime assumptions explicit:
|
||||
|
||||
- what needs network transport
|
||||
- what needs async tasking semantics
|
||||
- what needs code execution
|
||||
- what needs filesystem or persistence
|
||||
- what tools exist because of capabilities versus because of current native defaults
|
||||
|
||||
That is the set of seams worth discussing upstream.
|
||||
149
20260416_system_instructions.md
Normal file
149
20260416_system_instructions.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Browser System Instructions Injection
|
||||
|
||||
## Problem
|
||||
|
||||
The browser harness can run a real Codex turn, but the `wasm32` build does not
|
||||
load project docs from `AGENTS.md` today. In the browser, the application often
|
||||
knows important environment-specific policy that the harness does not know:
|
||||
|
||||
- code runs inside AppKernel;
|
||||
- browser APIs such as OPFS and network access are available;
|
||||
- only a specific OPFS path layout is allowed;
|
||||
- only specific GitHub URL prefixes are allowed; and
|
||||
- the agent is expected to fetch, cache, and search source code itself.
|
||||
|
||||
Those details need to reach the model as prompt-layer instructions even when the
|
||||
harness cannot discover them from the filesystem.
|
||||
|
||||
## Goals
|
||||
|
||||
- Let browser applications inject session-scoped prompt instructions.
|
||||
- Keep prompt roles distinct:
|
||||
- base instructions remain the base/system layer;
|
||||
- developer instructions carry application or runtime policy;
|
||||
- user instructions carry AGENTS/project-doc style content.
|
||||
- Keep responsibility boundaries clean:
|
||||
- the application decides where instructions come from;
|
||||
- the harness only accepts and applies resolved instruction text.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Teach the wasm harness to discover `AGENTS.md` on its own.
|
||||
- Move application-specific policy such as AppKernel, OPFS layout, or GitHub
|
||||
allowlists into `codex-core`.
|
||||
- Replace Codex's built-in base prompt.
|
||||
|
||||
## Existing State
|
||||
|
||||
`codex-core` already has the right internal prompt channels:
|
||||
|
||||
- `Config.base_instructions`
|
||||
- `Config.developer_instructions`
|
||||
- `Config.user_instructions`
|
||||
|
||||
The browser adapter does not expose them. `BrowserCodex` currently accepts an
|
||||
API key and a code executor, then builds a session config internally with a
|
||||
fixed cwd. The wasm project-doc loader is also stubbed out, so no `AGENTS.md`
|
||||
content is discovered automatically.
|
||||
|
||||
## Proposed API
|
||||
|
||||
Add a browser-session configuration object that the application can set before
|
||||
starting a turn:
|
||||
|
||||
```ts
|
||||
type BrowserSessionOptions = {
|
||||
cwd?: string;
|
||||
instructions?: {
|
||||
base?: string;
|
||||
developer?: string;
|
||||
user?: string;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Expose it through the browser harness as a session-scoped setter:
|
||||
|
||||
```ts
|
||||
const codex = new BrowserCodex(apiKey);
|
||||
codex.setSessionOptions({
|
||||
cwd: "/workspace",
|
||||
instructions: {
|
||||
developer: "...AppKernel/OPFS/network/GitHub policy...",
|
||||
user: "...resolved AGENTS.md text...",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Prompt Layer Mapping
|
||||
|
||||
### `instructions.developer`
|
||||
|
||||
Use for environment and policy details supplied by the browser application, for
|
||||
example:
|
||||
|
||||
- code runs inside AppKernel;
|
||||
- OPFS and network APIs are available;
|
||||
- approved OPFS path layout;
|
||||
- approved GitHub URL prefixes; and
|
||||
- fetch/cache/search expectations.
|
||||
|
||||
This maps to `Config.developer_instructions`.
|
||||
|
||||
### `instructions.user`
|
||||
|
||||
Use for application-resolved project docs such as `AGENTS.md`.
|
||||
|
||||
This maps to `Config.user_instructions`.
|
||||
|
||||
### `instructions.base`
|
||||
|
||||
Use only as an escape hatch when the embedding application intentionally wants
|
||||
to replace the base instruction bundle for the session.
|
||||
|
||||
This maps to `Config.base_instructions`.
|
||||
|
||||
Most callers should leave this unset so Codex keeps its built-in base prompt.
|
||||
|
||||
## Session Lifetime
|
||||
|
||||
These values are session-scoped, not turn-scoped.
|
||||
|
||||
If `cwd` or any injected instruction changes, the harness should discard the
|
||||
current browser session and create a new one on the next turn. This avoids
|
||||
mixing old prompt state with new environment policy.
|
||||
|
||||
## API Boundary
|
||||
|
||||
### Application responsibilities
|
||||
|
||||
- discover `AGENTS.md` or any equivalent project-doc source;
|
||||
- decide the approved OPFS layout;
|
||||
- decide the approved GitHub URL prefixes;
|
||||
- decide whether AppKernel/network guidance should be present; and
|
||||
- assemble those values into prompt text.
|
||||
|
||||
### Harness responsibilities
|
||||
|
||||
- accept already-resolved session options;
|
||||
- validate and store them;
|
||||
- apply them to the Codex session config;
|
||||
- ensure the configured cwd is used consistently for the session and for turn
|
||||
submission; and
|
||||
- recreate the session when session options change.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Keep the API additive. Existing callers that only set the API key and code
|
||||
executor should continue to work.
|
||||
- Reject unknown fields in the session-options payload so applications do not
|
||||
silently think a field is applied when it is ignored.
|
||||
- Apply the configured cwd both when building the browser config and when
|
||||
calling `Op::UserTurn`.
|
||||
|
||||
## Initial Scope
|
||||
|
||||
Implement the new `BrowserCodex` session-options setter in `wasm-harness`,
|
||||
apply it to config creation, and update the browser demo to act like an
|
||||
embedding application by supplying explicit developer instructions for the
|
||||
browser/AppKernel environment.
|
||||
427
20260416_wasm_appserver.md
Normal file
427
20260416_wasm_appserver.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# WASM Embedded App-Server Design
|
||||
|
||||
## TL;DR
|
||||
|
||||
The current browser harness wraps a single user submission by creating a thread, calling `thread.submit(Op::UserTurn { ... })`, and draining `thread.next_event()` until the turn completes. That path proved that the core harness can run in the browser, but it leaves the browser with a custom control surface that diverges from native app-server behavior and currently resets session state after every turn.
|
||||
|
||||
This document proposes switching the browser integration to an embedded in-process app-server runtime. The browser would keep a long-lived app-server instance in wasm, communicate with it through the existing app-server request and event model, and reuse the native app-server boundary instead of maintaining a browser-specific harness API.
|
||||
|
||||
## Objective
|
||||
|
||||
Use the existing app-server boundary as the primary control plane for the browser runtime so that:
|
||||
|
||||
- browser and native clients speak the same conceptual protocol
|
||||
- multi-turn behavior matches native behavior more closely
|
||||
- debugging and event inspection reuse existing app-server semantics
|
||||
- future features such as thread lifecycle, steering, approvals, and richer tooling do not require a second browser-specific orchestration layer
|
||||
|
||||
## Background
|
||||
|
||||
### Current Browser Design
|
||||
|
||||
The current browser runtime is centered on `BrowserCodex` in `codex-rs/wasm-harness/src/browser.rs`.
|
||||
|
||||
Its main flow is:
|
||||
|
||||
1. `BrowserCodex.submit_turn(prompt, on_event)` creates a fresh `LocalSet`.
|
||||
2. It either reuses or creates a `BrowserSession`.
|
||||
3. It submits a turn directly to core with:
|
||||
|
||||
```rust
|
||||
session.thread.submit(Op::UserTurn { ... }).await
|
||||
```
|
||||
|
||||
4. It loops on `session.thread.next_event().await` and forwards events to JS.
|
||||
5. When the turn finishes, it clears `self.session`.
|
||||
|
||||
This is a thin wrapper around the core thread API. It is effectively a `UserSubmit`-style interface: the browser gives the harness a prompt, the harness starts one turn, then pushes turn events back to JS.
|
||||
|
||||
The relevant behavior today is:
|
||||
|
||||
- `submit_turn()` scopes execution to a per-turn `LocalSet`
|
||||
- the browser wrapper owns a `BrowserSession { config, thread, session_configured }`
|
||||
- the wrapper emits raw core events directly to JS
|
||||
- the wrapper explicitly drops the session after each turn
|
||||
|
||||
### Why The Current Design Is Not Enough
|
||||
|
||||
The current design was useful as a proof of feasibility, but it creates several product and maintenance problems.
|
||||
|
||||
First, it is not actually aligned with the native client boundary. Native clients talk to app-server through requests, notifications, and streamed server events. The browser currently bypasses that layer and talks directly to `CodexThread`.
|
||||
|
||||
Second, it currently loses continuity across turns. Because `submit_turn()` uses a fresh `LocalSet` and clears `self.session` after each submission, the browser prototype starts a new harness session for every turn. That is why follow-up prompts appear to lack memory of earlier prompts.
|
||||
|
||||
Third, it forces the browser to maintain a separate orchestration contract. Any feature added at the app-server boundary has to be re-exposed or re-invented in the browser wrapper.
|
||||
|
||||
Fourth, it weakens debugging. Native app-server already has a request model, notification model, server-request model, and thread/turn lifecycle semantics. The browser wrapper currently exposes only a narrower direct-thread view.
|
||||
|
||||
## Why Switch To An Embedded App-Server
|
||||
|
||||
The app-server boundary already solves the problems the browser needs:
|
||||
|
||||
- request/response for typed client operations
|
||||
- notifications for fire-and-forget client messages
|
||||
- streamed server notifications for turn progress
|
||||
- server requests for approvals and similar interactive flows
|
||||
- explicit thread lifecycle APIs
|
||||
- a stable place to add future browser features without widening the direct core API
|
||||
|
||||
The repository already includes an in-process embedding path in `codex-rs/app-server/src/in_process.rs`. That runtime preserves app-server semantics while replacing stdio/websocket transport with in-memory channels.
|
||||
|
||||
That makes it a much better browser boundary than `BrowserCodex.submit_turn(...)`.
|
||||
|
||||
## Goals
|
||||
|
||||
- Preserve the existing app-server request and event model in the browser.
|
||||
- Keep a long-lived runtime alive across multiple browser turns.
|
||||
- Support multi-turn conversations without rebuilding browser-specific thread state each turn.
|
||||
- Reuse existing app-server features such as `thread/start`, `turn/start`, `turn/interrupt`, and server-driven approval requests.
|
||||
- Keep the wasm integration transport-local and in-process. The browser should not need to run a real socket server to use app-server semantics.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- This document does not require full native parity for browser tools, filesystem access, or sandboxing.
|
||||
- This document does not solve browser persistence by itself. State DB and rollout persistence still need separate wasm-capable implementations.
|
||||
- This document does not require reusing `run_main_with_transport(...)` directly. The proposal reuses the app-server boundary, not that exact native entrypoint.
|
||||
|
||||
## Proposal
|
||||
|
||||
### High-Level Design
|
||||
|
||||
Replace the current direct-thread browser wrapper with a wasm-facing wrapper around an embedded in-process app-server runtime.
|
||||
|
||||
This refactor should be a replacement, not an addition. We should remove the existing `BrowserCodex` implementation as part of the cleanup and move the current prototype onto the new app-server-based path.
|
||||
|
||||
The new stack would look like:
|
||||
|
||||
```text
|
||||
JS UI
|
||||
-> wasm wrapper
|
||||
-> in-process app-server runtime
|
||||
-> MessageProcessor
|
||||
-> ThreadManager / Codex core
|
||||
```
|
||||
|
||||
Instead of calling `thread.submit(Op::UserTurn { ... })` directly, the browser would:
|
||||
|
||||
- start one in-process app-server runtime for the browser session
|
||||
- send app-server client requests into that runtime
|
||||
- consume app-server server notifications and server requests from that runtime
|
||||
|
||||
### Browser Boundary
|
||||
|
||||
The wasm wrapper should expose an API shaped like the app-server protocol rather than the current `submit_turn(prompt, on_event)` helper.
|
||||
|
||||
A minimal JS-facing API is:
|
||||
|
||||
- `start(options) -> handle`
|
||||
- `request(request) -> Promise<response>`
|
||||
- `notify(notification) -> Promise<void>` or `void`
|
||||
- `nextEvent() -> Promise<event | null>`
|
||||
- `respondToServerRequest(requestId, result) -> Promise<void>`
|
||||
- `failServerRequest(requestId, error) -> Promise<void>`
|
||||
- `shutdown() -> Promise<void>`
|
||||
|
||||
This mirrors the capabilities already present on `InProcessClientHandle`:
|
||||
|
||||
- `request(...)`
|
||||
- `notify(...)`
|
||||
- `next_event()`
|
||||
- `respond_to_server_request(...)`
|
||||
- `fail_server_request(...)`
|
||||
- `shutdown()`
|
||||
|
||||
### Runtime Lifecycle
|
||||
|
||||
The browser should create one long-lived in-process app-server runtime and keep it alive until the browser session is reset or closed.
|
||||
|
||||
Expected lifecycle:
|
||||
|
||||
1. Browser constructs wasm wrapper.
|
||||
2. Wrapper builds `InProcessStartArgs`.
|
||||
3. Wrapper calls `codex_app_server::in_process::start(...)`.
|
||||
4. Wrapper keeps the returned handle alive across turns.
|
||||
5. JS sends requests such as `thread/start` and `turn/start`.
|
||||
6. JS drains streamed server events with `nextEvent()`.
|
||||
7. On shutdown or reset, wrapper calls `shutdown()`.
|
||||
|
||||
This is the key change from the current design. The runtime is session-scoped, not turn-scoped.
|
||||
|
||||
### Message Flow
|
||||
|
||||
For a typical first turn:
|
||||
|
||||
1. JS calls `request(thread/start { ... })`.
|
||||
2. The app-server returns a thread id.
|
||||
3. JS calls `request(turn/start { threadId, input, ... })`.
|
||||
4. The app-server returns an in-progress turn response.
|
||||
5. JS repeatedly calls `nextEvent()` or receives pushed events.
|
||||
6. The runtime emits `turn/started`, item deltas, tool events, and `turn/completed`.
|
||||
|
||||
For later turns:
|
||||
|
||||
1. JS reuses the existing thread id.
|
||||
2. JS calls `request(turn/start { threadId, input, ... })` again.
|
||||
3. The same app-server runtime and the same underlying thread/session continue processing.
|
||||
|
||||
This aligns browser behavior with native behavior and removes the current per-turn session reset.
|
||||
|
||||
### Event Model
|
||||
|
||||
The browser should consume app-server events, not raw core `EventMsg` values.
|
||||
|
||||
That gives the browser:
|
||||
|
||||
- a stable protocol-shaped event stream
|
||||
- server notifications for turn lifecycle and content updates
|
||||
- server requests for approvals and other interactive flows
|
||||
- lag/backpressure signals already defined by the in-process embedding
|
||||
|
||||
It also gives us a cleaner debugging surface because the browser can log:
|
||||
|
||||
- outgoing client requests
|
||||
- client notifications
|
||||
- incoming server notifications
|
||||
- incoming server requests
|
||||
- responses to server requests
|
||||
|
||||
### Why Use `in_process` Instead Of `run_main_with_transport`
|
||||
|
||||
`run_main_with_transport(...)` is not the right wasm entrypoint.
|
||||
|
||||
It hardcodes:
|
||||
|
||||
- stdio or websocket transport startup
|
||||
- native signal handling
|
||||
- native logging and DB startup assumptions
|
||||
|
||||
By contrast, `in_process.rs` already does the important part we want:
|
||||
|
||||
- run `MessageProcessor`
|
||||
- keep the app-server request/notification/event contract
|
||||
- replace transport with in-memory channels
|
||||
|
||||
So the design should reuse app-server semantics through `in_process`, not try to reuse the native transport bootstrap unchanged.
|
||||
|
||||
## Detailed Design
|
||||
|
||||
### 1. Replace `BrowserCodex` With A WASM App-Server Wrapper
|
||||
|
||||
Add a new browser-facing wrapper that owns:
|
||||
|
||||
- the long-lived in-process app-server handle
|
||||
- browser-specific runtime services such as the code executor bridge
|
||||
- browser session options needed to build config and initialize params
|
||||
|
||||
This wrapper replaces `BrowserCodex` as the main orchestration entrypoint.
|
||||
|
||||
As part of this refactor we should:
|
||||
|
||||
- remove the existing direct-thread `BrowserCodex` implementation
|
||||
- remove `BrowserSession` as a browser-specific wrapper around `CodexThread`
|
||||
- update the current browser prototype to construct and use the new app-server-based wrapper instead
|
||||
|
||||
We do not want to maintain two browser orchestration paths. The prototype should become the first consumer of the new design.
|
||||
|
||||
### 2. Request Serialization
|
||||
|
||||
The wrapper should accept JSON or `JsValue` payloads that correspond to app-server requests and notifications.
|
||||
|
||||
At the wasm boundary:
|
||||
|
||||
- JS passes a request object
|
||||
- wasm deserializes into `ClientRequest` or `ClientNotification`
|
||||
- `in_process` handles the request
|
||||
- wasm serializes responses and events back to JS
|
||||
|
||||
This keeps the browser API close to the existing protocol and avoids introducing a second custom Rust-to-JS command language.
|
||||
|
||||
### 3. Thread Ownership
|
||||
|
||||
The browser should treat thread ids as app-server resources, not as direct `CodexThread` handles.
|
||||
|
||||
That means:
|
||||
|
||||
- creating threads through `thread/start`
|
||||
- reading state through `thread/read` and related APIs
|
||||
- starting turns through `turn/start`
|
||||
- steering or interrupting turns through existing turn APIs
|
||||
|
||||
This is an important layering choice. The browser should stop owning direct thread runtime objects.
|
||||
|
||||
### 4. Approval And Server Request Handling
|
||||
|
||||
The browser must support app-server initiated requests back to the client.
|
||||
|
||||
Examples include:
|
||||
|
||||
- tool approval requests
|
||||
- user input requests
|
||||
- future browser-specific interactive flows
|
||||
|
||||
When `nextEvent()` yields a `ServerRequest`, JS must either:
|
||||
|
||||
- answer with `respondToServerRequest(...)`
|
||||
- or reject with `failServerRequest(...)`
|
||||
|
||||
This is a capability the current direct-thread wrapper does not model cleanly.
|
||||
|
||||
### 5. Debugging
|
||||
|
||||
The browser wrapper should log the app-server boundary directly.
|
||||
|
||||
Recommended browser-side logging:
|
||||
|
||||
- every outgoing request with method and id
|
||||
- every request result
|
||||
- every incoming server notification
|
||||
- every incoming server request
|
||||
- every reply to a server request
|
||||
- backpressure or lag markers
|
||||
|
||||
This is a better debugging surface than ad hoc logging around direct `submit()` and `next_event()` calls because it reflects the real control plane used by native clients.
|
||||
|
||||
## Storage And Persistence
|
||||
|
||||
This proposal improves the control plane, but it does not by itself provide browser persistence.
|
||||
|
||||
There are two separate persistence problems:
|
||||
|
||||
1. Core state DB
|
||||
Core currently initializes session storage internally. On `wasm32`, the current state DB bridge is stubbed and returns `None`.
|
||||
|
||||
2. Rollout persistence
|
||||
The current wasm rollout recorder is also stubbed. Recording is effectively a no-op and history reload is unavailable.
|
||||
|
||||
As a result, the embedded app-server design gives us:
|
||||
|
||||
- live multi-turn continuity within a running browser session
|
||||
|
||||
But it does not yet give us:
|
||||
|
||||
- browser reload/resume
|
||||
- durable thread metadata
|
||||
- durable rollout history
|
||||
|
||||
Those require follow-on work below the `in_process` layer.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Milestone 1: Replace The Prototype Runtime With `in_process`
|
||||
|
||||
Build the wasm wrapper on top of `codex_app_server::in_process` and migrate the existing prototype to use it.
|
||||
|
||||
Scope:
|
||||
|
||||
- long-lived runtime
|
||||
- request/notification/event API exposed to JS
|
||||
- thread and turn flow through app-server methods
|
||||
- browser code executor still supplied through existing wasm/runtime hooks
|
||||
- remove the direct `BrowserCodex` / `BrowserSession` path from `wasm-harness`
|
||||
|
||||
Success criteria:
|
||||
|
||||
- multi-turn browser session works without resetting runtime state between turns
|
||||
- browser logs show app-server request and event traffic
|
||||
- browser no longer calls `thread.submit(Op::UserTurn { ... })` directly
|
||||
- the existing browser prototype runs on the app-server-based implementation
|
||||
- there is only one supported browser runtime path in the codebase
|
||||
|
||||
### Milestone 2: Align With `codex-app-server-client`
|
||||
|
||||
Decide whether the wasm wrapper should sit directly on `in_process` or on a thinner variant of `codex-app-server-client`.
|
||||
|
||||
The client facade is appealing because it already wraps:
|
||||
|
||||
- in-process runtime startup
|
||||
- event forwarding
|
||||
- server-request resolution helpers
|
||||
- convergence with the remote app-server client shape
|
||||
|
||||
This may reduce custom browser-side orchestration code.
|
||||
|
||||
### Milestone 3: Browser Persistence
|
||||
|
||||
Add real wasm-backed persistence for:
|
||||
|
||||
- rollout history
|
||||
- thread metadata
|
||||
- any state DB-backed browser features we want to preserve across reloads
|
||||
|
||||
This likely requires explicit storage seams below the app-server wrapper.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Keep The Current `BrowserCodex` Model And Preserve Session State
|
||||
|
||||
We could keep the current wrapper and only stop clearing `self.session`.
|
||||
|
||||
That would likely fix the immediate memory bug, but it would not fix the larger architectural problem:
|
||||
|
||||
- the browser would still use a custom direct-thread boundary
|
||||
- app-server APIs would still need browser-specific re-exposure
|
||||
- debugging would still happen at a less stable abstraction level
|
||||
|
||||
This is a tactical fix, not the design we want to converge on.
|
||||
|
||||
### Reuse `run_main_with_transport(...)` Directly
|
||||
|
||||
We could try to add a custom wasm transport and plug it into `run_main_with_transport(...)`.
|
||||
|
||||
This is not attractive because the function currently bundles:
|
||||
|
||||
- transport startup
|
||||
- native shutdown handling
|
||||
- logging and telemetry bootstrap
|
||||
- native transport assumptions
|
||||
|
||||
`in_process` is already a better split for embedding.
|
||||
|
||||
## Risks
|
||||
|
||||
- The browser protocol surface becomes closer to app-server, which may require slightly more client-side plumbing than the current `submit_turn(prompt)` helper.
|
||||
- Some app-server internals still assume native runtime pieces and may need additional injection points for wasm.
|
||||
- Storage is still unresolved. This design fixes control-plane drift first, not persistence.
|
||||
- If we expose raw JSON-RPC too literally to JS, the browser API may become awkward. We should keep the protocol shape while still providing small ergonomic helpers.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should the wasm wrapper expose raw JSON-RPC payloads, typed helper methods, or both?
|
||||
2. Should the browser wrap `in_process` directly, or should it reuse a slimmer `codex-app-server-client` facade?
|
||||
3. Which app-server methods do we want to support in browser v1 beyond `thread/start` and `turn/start`?
|
||||
4. Do we want server events delivered by pull (`nextEvent`) only, or also by JS callback subscription?
|
||||
5. What is the right storage abstraction for browser-backed rollout and state DB persistence?
|
||||
|
||||
## Recommended Decisions For Implementation Start
|
||||
|
||||
To begin implementation, we should make the following decisions explicit.
|
||||
|
||||
- Browser API surface
|
||||
Recommendation:
|
||||
Expose a thin typed wrapper over the app-server protocol rather than raw JSON-RPC only. The JS API should provide ergonomic helpers such as `startThread`, `startTurn`, `interruptTurn`, `nextEvent`, and `respondToServerRequest`, while staying close to app-server request and event types under the hood.
|
||||
|
||||
- `in_process` vs `codex-app-server-client`
|
||||
Recommendation:
|
||||
Start directly on `codex_app_server::in_process`. It is the simpler runtime dependency and gives us direct control in wasm. We should borrow the client facade's worker and event-forwarding patterns where useful, but avoid adding a second abstraction layer unless the wasm wrapper clearly grows into it.
|
||||
|
||||
- Session lifecycle
|
||||
Recommendation:
|
||||
Create one long-lived embedded app-server runtime per browser session and keep it alive until explicit reset or shutdown. Runtime lifetime should be browser-session scoped, not turn scoped. Reset should happen only on explicit session teardown, configuration changes that require rebuild, or API key changes.
|
||||
|
||||
- Event delivery model
|
||||
Recommendation:
|
||||
Use callback subscription as the primary browser-facing event model, while optionally retaining a polling escape hatch for tests or simple consumers. Internally we can still drain `next_event()`, but most browser UI code is simpler if events are pushed into JS callbacks.
|
||||
|
||||
- Server request handling
|
||||
Recommendation:
|
||||
Treat server requests as first-class and require explicit client handling. The wrapper should surface every `ServerRequest` to JS and require the browser to answer or reject it. For unsupported request types in browser v1, reject them explicitly with a clear error rather than letting turns hang.
|
||||
|
||||
- v1 app-server method set
|
||||
Recommendation:
|
||||
Support a narrow but complete thread and turn slice in browser v1: `thread/start`, `thread/read`, `turn/start`, `turn/interrupt`, and `turn/steer`. That is enough for a real multi-turn conversational product and debugging workflow without taking on the full app-server surface immediately.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Move the browser runtime to an embedded in-process app-server boundary.
|
||||
|
||||
That keeps the browser on the same architectural path as native clients, fixes the current per-turn session model, improves debugging, and creates a better long-term seam for browser-specific runtime and storage work.
|
||||
6
MODULE.bazel.lock
generated
6
MODULE.bazel.lock
generated
@@ -674,8 +674,6 @@
|
||||
"byteorder_1.5.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"std\"],\"i128\":[],\"std\":[]}}",
|
||||
"bytes_1.11.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"extra-platforms\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.60\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"bytestring_1.5.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{\"serde\":[\"dep:serde_core\"]}}",
|
||||
"bzip2-sys_0.1.13+1.0.8": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"}],\"features\":{\"__disabled\":[],\"static\":[]}}",
|
||||
"bzip2_0.5.2": "{\"dependencies\":[{\"name\":\"bzip2-sys\",\"optional\":true,\"req\":\"^0.1.13\"},{\"default_features\":false,\"features\":[\"rust-allocator\",\"semver-prefix\"],\"name\":\"libbz2-rs-sys\",\"optional\":true,\"req\":\"^0.1.3\"},{\"features\":[\"quickcheck1\"],\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.5.4\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"default\":[\"dep:bzip2-sys\"],\"libbz2-rs-sys\":[\"dep:libbz2-rs-sys\",\"bzip2-sys?/__disabled\"],\"static\":[\"bzip2-sys?/static\"]}}",
|
||||
"cached_0.56.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.6\"},{\"name\":\"async-trait\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"cached_proc_macro\",\"optional\":true,\"req\":\"^0.25.0\"},{\"name\":\"cached_proc_macro_types\",\"optional\":true,\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"copy_dir\",\"req\":\"^0.1.3\"},{\"name\":\"directories\",\"optional\":true,\"req\":\"^6.0\"},{\"default_features\":false,\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"googletest\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"features\":[\"inline-more\"],\"name\":\"hashbrown\",\"req\":\"^0.15\"},{\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"r2d2\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"r2d2\"],\"name\":\"redis\",\"optional\":true,\"req\":\"^0.32\"},{\"name\":\"rmp-serde\",\"optional\":true,\"req\":\"^1.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"name\":\"sled\",\"optional\":true,\"req\":\"^0.34\"},{\"kind\":\"dev\",\"name\":\"smartstring\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"macros\",\"time\",\"sync\",\"parking_lot\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"web-time\",\"req\":\"^1.1.0\"}],\"features\":{\"ahash\":[\"dep:ahash\",\"hashbrown/default\"],\"async\":[\"futures\",\"tokio\",\"async-trait\"],\"async_tokio_rt_multi_thread\":[\"async\",\"tokio/rt-multi-thread\"],\"default\":[\"proc_macro\",\"ahash\"],\"disk_store\":[\"sled\",\"serde\",\"rmp-serde\",\"directories\"],\"proc_macro\":[\"cached_proc_macro\",\"cached_proc_macro_types\"],\"redis_ahash\":[\"redis_store\",\"redis/ahash\"],\"redis_async_std\":[\"redis_store\",\"async\",\"redis/aio\",\"redis/async-std-comp\",\"redis/tls\",\"redis/async-std-tls-comp\"],\"redis_connection_manager\":[\"redis_store\",\"redis/connection-manager\"],\"redis_store\":[\"redis\",\"r2d2\",\"serde\",\"serde_json\"],\"redis_tokio\":[\"redis_store\",\"async\",\"redis/aio\",\"redis/tokio-comp\",\"redis/tls\",\"redis/tokio-native-tls-comp\"],\"wasm\":[]}}",
|
||||
"cached_proc_macro_0.25.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.20.8\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.49\"},{\"name\":\"quote\",\"req\":\"^1.0.6\"},{\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}",
|
||||
"cached_proc_macro_types_0.1.1": "{\"dependencies\":[],\"features\":{}}",
|
||||
@@ -770,7 +768,6 @@
|
||||
"deadpool_0.12.3": "{\"dependencies\":[{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.0\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"config\",\"req\":\"^0.15\"},{\"features\":[\"html_reports\",\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"deadpool-runtime\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"name\":\"num_cpus\",\"req\":\"^1.11.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.5\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.5.0\"}],\"features\":{\"default\":[\"managed\",\"unmanaged\"],\"managed\":[],\"rt_async-std_1\":[\"deadpool-runtime/async-std_1\"],\"rt_tokio_1\":[\"deadpool-runtime/tokio_1\"],\"unmanaged\":[]}}",
|
||||
"debugid_0.8.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.85\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.37\"},{\"name\":\"uuid\",\"req\":\"^1.0.0\"}],\"features\":{}}",
|
||||
"debugserver-types_0.5.0": "{\"dependencies\":[{\"name\":\"schemafy\",\"req\":\"^0.5.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}",
|
||||
"deflate64_0.1.10": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.13.1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.7.1\"}],\"features\":{}}",
|
||||
"der-parser_10.0.0": "{\"dependencies\":[{\"name\":\"asn1-rs\",\"req\":\"^0.7\"},{\"name\":\"bitvec\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"cookie-factory\",\"optional\":true,\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"nom\",\"req\":\"^7.0\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.0\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"test-case\",\"req\":\"^3.0\"}],\"features\":{\"as_bitvec\":[\"bitvec\"],\"bigint\":[\"num-bigint\"],\"default\":[\"std\"],\"serialize\":[\"std\",\"cookie-factory\"],\"std\":[],\"unstable\":[]}}",
|
||||
"der_0.7.10": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3\"},{\"default_features\":false,\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"const-oid\",\"optional\":true,\"req\":\"^0.9.2\"},{\"name\":\"der_derive\",\"optional\":true,\"req\":\"^0.7.2\"},{\"name\":\"flagset\",\"optional\":true,\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.1\"},{\"features\":[\"alloc\"],\"name\":\"pem-rfc7468\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"zeroize?/alloc\"],\"arbitrary\":[\"dep:arbitrary\",\"const-oid?/arbitrary\",\"std\"],\"bytes\":[\"dep:bytes\",\"alloc\"],\"derive\":[\"dep:der_derive\"],\"oid\":[\"dep:const-oid\"],\"pem\":[\"dep:pem-rfc7468\",\"alloc\",\"zeroize\"],\"real\":[],\"std\":[\"alloc\"]}}",
|
||||
"deranged_0.5.5": "{\"dependencies\":[{\"name\":\"deranged-macros\",\"optional\":true,\"req\":\"=0.3.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.15\"},{\"default_features\":false,\"name\":\"powerfmt\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1.0.3\"},{\"default_features\":false,\"name\":\"rand08\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"rand08\",\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"name\":\"rand09\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand09\",\"package\":\"rand\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.86\"}],\"features\":{\"alloc\":[],\"default\":[],\"macros\":[\"dep:deranged-macros\"],\"num\":[\"dep:num-traits\"],\"powerfmt\":[\"dep:powerfmt\"],\"quickcheck\":[\"dep:quickcheck\",\"alloc\"],\"rand\":[\"rand08\",\"rand09\"],\"rand08\":[\"dep:rand08\"],\"rand09\":[\"dep:rand09\"],\"serde\":[\"dep:serde_core\"]}}",
|
||||
@@ -1026,8 +1023,6 @@
|
||||
"lru_0.12.5": "{\"dependencies\":[{\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"scoped_threadpool\",\"req\":\"0.1.*\"},{\"kind\":\"dev\",\"name\":\"stats_alloc\",\"req\":\"0.1.*\"}],\"features\":{\"default\":[\"hashbrown\"],\"nightly\":[\"hashbrown\",\"hashbrown/nightly\"]}}",
|
||||
"lru_0.16.3": "{\"dependencies\":[{\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.16.0\"},{\"kind\":\"dev\",\"name\":\"scoped_threadpool\",\"req\":\"0.1.*\"},{\"kind\":\"dev\",\"name\":\"stats_alloc\",\"req\":\"0.1.*\"}],\"features\":{\"default\":[\"hashbrown\"],\"nightly\":[\"hashbrown\",\"hashbrown/nightly\"]}}",
|
||||
"lsp-types_0.94.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^1.0.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.34\"},{\"name\":\"serde_json\",\"req\":\"^1.0.50\"},{\"name\":\"serde_repr\",\"req\":\"^0.1\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.0.0\"}],\"features\":{\"default\":[],\"proposed\":[]}}",
|
||||
"lzma-rs_0.3.0": "{\"dependencies\":[{\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"name\":\"crc\",\"req\":\"^3.0.0\"},{\"name\":\"env_logger\",\"optional\":true,\"req\":\"^0.9.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"rust-lzma\",\"req\":\"^0.5\"}],\"features\":{\"enable_logging\":[\"env_logger\",\"log\"],\"raw_decoder\":[],\"stream\":[]}}",
|
||||
"lzma-sys_0.1.20": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.34\"},{\"name\":\"libc\",\"req\":\"^0.2.51\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.14\"}],\"features\":{\"static\":[]}}",
|
||||
"mach2_0.4.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"}],\"features\":{\"default\":[],\"unstable\":[]}}",
|
||||
"maplit_1.0.2": "{\"dependencies\":[],\"features\":{}}",
|
||||
"matchers_0.2.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"syntax\",\"dfa-build\",\"dfa-search\"],\"name\":\"regex-automata\",\"req\":\"^0.4\"}],\"features\":{\"unicode\":[\"regex-automata/unicode\"]}}",
|
||||
@@ -1603,7 +1598,6 @@
|
||||
"x509-parser_0.18.1": "{\"dependencies\":[{\"features\":[\"datetime\"],\"name\":\"asn1-rs\",\"req\":\"^0.7.0\"},{\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"data-encoding\",\"req\":\"^2.2.1\"},{\"features\":[\"bigint\"],\"name\":\"der-parser\",\"req\":\"^10.0\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"nom\",\"req\":\"^7.0\"},{\"features\":[\"crypto\",\"x509\",\"x962\"],\"name\":\"oid-registry\",\"req\":\"^0.8.1\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17.12\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.35\"}],\"features\":{\"default\":[],\"validate\":[],\"verify\":[\"ring\"],\"verify-aws\":[\"aws-lc-rs\"]}}",
|
||||
"xattr_1.6.1": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.150\",\"target\":\"cfg(any(target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\", target_os = \\\"macos\\\", target_os = \\\"hurd\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"default\":[\"unsupported\"],\"unsupported\":[]}}",
|
||||
"xdg-home_1.3.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
|
||||
"xz2_0.1.7": "{\"dependencies\":[{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.1.26\"},{\"name\":\"lzma-sys\",\"req\":\"^0.1.18\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"tokio-core\",\"req\":\"^0.1.17\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.12\"}],\"features\":{\"static\":[\"lzma-sys/static\"],\"tokio\":[\"tokio-io\",\"futures\"]}}",
|
||||
"yaml-rust_0.4.5": "{\"dependencies\":[{\"name\":\"linked-hash-map\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"}],\"features\":{}}",
|
||||
"yansi_1.0.1": "{\"dependencies\":[{\"name\":\"is-terminal\",\"optional\":true,\"req\":\"^0.4.11\"}],\"features\":{\"_nightly\":[],\"alloc\":[],\"default\":[\"std\"],\"detect-env\":[\"std\"],\"detect-tty\":[\"is-terminal\",\"std\"],\"hyperlink\":[\"std\"],\"std\":[\"alloc\"]}}",
|
||||
"yasna_0.5.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.6.1\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.1\"}],\"features\":{\"default\":[],\"std\":[]}}",
|
||||
|
||||
93
codex-rs/Cargo.lock
generated
93
codex-rs/Cargo.lock
generated
@@ -1051,25 +1051,6 @@ dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.13+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cached"
|
||||
version = "0.56.0"
|
||||
@@ -1386,6 +1367,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tungstenite",
|
||||
"url",
|
||||
"wasm-bindgen-futures",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
@@ -1683,6 +1665,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"wasm-bindgen-futures",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
@@ -1763,6 +1746,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -1879,6 +1863,7 @@ dependencies = [
|
||||
"image",
|
||||
"indexmap 2.13.0",
|
||||
"insta",
|
||||
"js-sys",
|
||||
"landlock",
|
||||
"libc",
|
||||
"maplit",
|
||||
@@ -2923,6 +2908,27 @@ dependencies = [
|
||||
"v8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-wasm-harness"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-app-server-protocol",
|
||||
"codex-code-mode",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-protocol",
|
||||
"js-sys",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-windows-sandbox"
|
||||
version = "0.0.0"
|
||||
@@ -3595,12 +3601,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -5900,27 +5900,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
@@ -11878,15 +11857,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
@@ -12097,28 +12067,15 @@ version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"deflate64",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"hmac",
|
||||
"indexmap 2.13.0",
|
||||
"lzma-rs",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"xz2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -52,6 +52,7 @@ members = [
|
||||
"tui",
|
||||
"tools",
|
||||
"v8-poc",
|
||||
"wasm-harness",
|
||||
"utils/absolute-path",
|
||||
"utils/cargo-bin",
|
||||
"git-utils",
|
||||
@@ -229,6 +230,7 @@ insta = "1.46.3"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
jsonwebtoken = "9.3.1"
|
||||
js-sys = "0.3.85"
|
||||
keyring = { version = "3.6", default-features = false }
|
||||
landlock = "0.4.4"
|
||||
lazy_static = "1"
|
||||
@@ -335,10 +337,13 @@ urlencoding = "2.1"
|
||||
uuid = "1"
|
||||
vt100 = "0.16.2"
|
||||
walkdir = "2.5.0"
|
||||
wasm-bindgen = "0.2.108"
|
||||
wasm-bindgen-futures = "0.4.58"
|
||||
web-sys = "0.3.85"
|
||||
webbrowser = "1.0"
|
||||
which = "8"
|
||||
wildmatch = "2.6.1"
|
||||
zip = "2.4.2"
|
||||
zip = { version = "2.4.2", default-features = false, features = ["deflate"] }
|
||||
zstd = "0.13"
|
||||
|
||||
wiremock = "0.6"
|
||||
|
||||
@@ -15,16 +15,18 @@ workspace = true
|
||||
[dependencies]
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha1 = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
codex-login = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
mod analytics_client;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
include!("native.rs");
|
||||
|
||||
pub use analytics_client::AnalyticsEventsClient;
|
||||
pub use analytics_client::AnalyticsFact;
|
||||
pub use analytics_client::AnalyticsReducer;
|
||||
pub use analytics_client::AppInvocation;
|
||||
pub use analytics_client::AppMentionedInput;
|
||||
pub use analytics_client::AppUsedInput;
|
||||
pub use analytics_client::CustomAnalyticsFact;
|
||||
pub use analytics_client::InvocationType;
|
||||
pub use analytics_client::PluginState;
|
||||
pub use analytics_client::PluginStateChangedInput;
|
||||
pub use analytics_client::PluginUsedInput;
|
||||
pub use analytics_client::SkillInvocation;
|
||||
pub use analytics_client::SkillInvokedInput;
|
||||
pub use analytics_client::TrackEventsContext;
|
||||
pub use analytics_client::build_track_events_context;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm::*;
|
||||
|
||||
17
codex-rs/analytics/src/native.rs
Normal file
17
codex-rs/analytics/src/native.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod analytics_client;
|
||||
|
||||
pub use analytics_client::AnalyticsEventsClient;
|
||||
pub use analytics_client::AnalyticsFact;
|
||||
pub use analytics_client::AnalyticsReducer;
|
||||
pub use analytics_client::AppInvocation;
|
||||
pub use analytics_client::AppMentionedInput;
|
||||
pub use analytics_client::AppUsedInput;
|
||||
pub use analytics_client::CustomAnalyticsFact;
|
||||
pub use analytics_client::InvocationType;
|
||||
pub use analytics_client::PluginState;
|
||||
pub use analytics_client::PluginStateChangedInput;
|
||||
pub use analytics_client::PluginUsedInput;
|
||||
pub use analytics_client::SkillInvocation;
|
||||
pub use analytics_client::SkillInvokedInput;
|
||||
pub use analytics_client::TrackEventsContext;
|
||||
pub use analytics_client::build_track_events_context;
|
||||
170
codex-rs/analytics/src/wasm.rs
Normal file
170
codex-rs/analytics/src/wasm.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TrackEventsContext {
|
||||
pub model_slug: String,
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
}
|
||||
|
||||
pub fn build_track_events_context(
|
||||
model_slug: String,
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
) -> TrackEventsContext {
|
||||
TrackEventsContext {
|
||||
model_slug,
|
||||
thread_id,
|
||||
turn_id,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SkillInvocation {
|
||||
pub skill_name: String,
|
||||
pub skill_scope: SkillScope,
|
||||
pub skill_path: PathBuf,
|
||||
pub invocation_type: InvocationType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum InvocationType {
|
||||
Explicit,
|
||||
Implicit,
|
||||
}
|
||||
|
||||
pub struct AppInvocation {
|
||||
pub connector_id: Option<String>,
|
||||
pub app_name: Option<String>,
|
||||
pub invocation_type: Option<InvocationType>,
|
||||
}
|
||||
|
||||
pub enum AnalyticsFact {
|
||||
Initialize {
|
||||
connection_id: u64,
|
||||
params: codex_app_server_protocol::InitializeParams,
|
||||
},
|
||||
Request {
|
||||
connection_id: u64,
|
||||
request_id: codex_app_server_protocol::RequestId,
|
||||
request: Box<codex_app_server_protocol::ClientRequest>,
|
||||
},
|
||||
Response {
|
||||
connection_id: u64,
|
||||
response: Box<codex_app_server_protocol::ClientResponse>,
|
||||
},
|
||||
Notification(Box<codex_app_server_protocol::ServerNotification>),
|
||||
Custom(CustomAnalyticsFact),
|
||||
}
|
||||
|
||||
pub enum CustomAnalyticsFact {
|
||||
SkillInvoked(SkillInvokedInput),
|
||||
AppMentioned(AppMentionedInput),
|
||||
AppUsed(AppUsedInput),
|
||||
PluginUsed(PluginUsedInput),
|
||||
PluginStateChanged(PluginStateChangedInput),
|
||||
}
|
||||
|
||||
pub struct SkillInvokedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub invocations: Vec<SkillInvocation>,
|
||||
}
|
||||
|
||||
pub struct AppMentionedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub mentions: Vec<AppInvocation>,
|
||||
}
|
||||
|
||||
pub struct AppUsedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub app: AppInvocation,
|
||||
}
|
||||
|
||||
pub struct PluginUsedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub plugin: PluginTelemetryMetadata,
|
||||
}
|
||||
|
||||
pub struct PluginStateChangedInput {
|
||||
pub plugin: PluginTelemetryMetadata,
|
||||
pub state: PluginState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum PluginState {
|
||||
Installed,
|
||||
Uninstalled,
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AnalyticsReducer;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AnalyticsEventsClient {
|
||||
analytics_enabled: Option<bool>,
|
||||
_marker: Arc<()>,
|
||||
}
|
||||
|
||||
impl AnalyticsEventsClient {
|
||||
pub fn new<T>(
|
||||
_auth_manager: Arc<T>,
|
||||
_base_url: String,
|
||||
analytics_enabled: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
analytics_enabled,
|
||||
_marker: Arc::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_skill_invocations(
|
||||
&self,
|
||||
_tracking: TrackEventsContext,
|
||||
_invocations: Vec<SkillInvocation>,
|
||||
) {
|
||||
let _ = self.analytics_enabled;
|
||||
}
|
||||
|
||||
pub fn track_app_mentioned(
|
||||
&self,
|
||||
_tracking: TrackEventsContext,
|
||||
_mentions: Vec<AppInvocation>,
|
||||
) {
|
||||
let _ = self.analytics_enabled;
|
||||
}
|
||||
|
||||
pub fn track_app_used(&self, _tracking: TrackEventsContext, _app: AppInvocation) {
|
||||
let _ = self.analytics_enabled;
|
||||
}
|
||||
|
||||
pub fn track_plugin_used(
|
||||
&self,
|
||||
_tracking: TrackEventsContext,
|
||||
_plugin: PluginTelemetryMetadata,
|
||||
) {
|
||||
let _ = self.analytics_enabled;
|
||||
}
|
||||
|
||||
pub fn track_plugin_installed(&self, _plugin: PluginTelemetryMetadata) {
|
||||
let _ = self.analytics_enabled;
|
||||
}
|
||||
|
||||
pub fn track_plugin_uninstalled(&self, _plugin: PluginTelemetryMetadata) {
|
||||
let _ = self.analytics_enabled;
|
||||
}
|
||||
|
||||
pub fn track_plugin_enabled(&self, _plugin: PluginTelemetryMetadata) {
|
||||
let _ = self.analytics_enabled;
|
||||
}
|
||||
|
||||
pub fn track_plugin_disabled(&self, _plugin: PluginTelemetryMetadata) {
|
||||
let _ = self.analytics_enabled;
|
||||
}
|
||||
}
|
||||
@@ -25,16 +25,18 @@ serde_with = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ts-rs = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"base64",
|
||||
"macros",
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
ts-rs = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -5295,6 +5295,7 @@ impl McpServerElicitationAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl From<McpServerElicitationAction> for rmcp::model::ElicitationAction {
|
||||
fn from(value: McpServerElicitationAction) -> Self {
|
||||
match value {
|
||||
@@ -5305,6 +5306,7 @@ impl From<McpServerElicitationAction> for rmcp::model::ElicitationAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl From<rmcp::model::ElicitationAction> for McpServerElicitationAction {
|
||||
fn from(value: rmcp::model::ElicitationAction) -> Self {
|
||||
match value {
|
||||
@@ -5722,6 +5724,7 @@ pub struct McpServerElicitationRequestResponse {
|
||||
pub meta: Option<JsonValue>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl From<McpServerElicitationRequestResponse> for rmcp::model::CreateElicitationResult {
|
||||
fn from(value: McpServerElicitationRequestResponse) -> Self {
|
||||
Self {
|
||||
@@ -5731,6 +5734,7 @@ impl From<McpServerElicitationRequestResponse> for rmcp::model::CreateElicitatio
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl From<rmcp::model::CreateElicitationResult> for McpServerElicitationRequestResponse {
|
||||
fn from(value: rmcp::model::CreateElicitationResult) -> Self {
|
||||
Self {
|
||||
@@ -7186,6 +7190,7 @@ mod tests {
|
||||
assert_eq!(reason, Some("askForApproval.granular"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn mcp_server_elicitation_response_round_trips_rmcp_result() {
|
||||
let rmcp_result = rmcp::model::CreateElicitationResult {
|
||||
|
||||
@@ -19,6 +19,8 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tree-sitter = { workspace = true }
|
||||
tree-sitter-bash = { workspace = true }
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ use crate::ApplyPatchFileChange;
|
||||
use crate::ApplyPatchFileUpdate;
|
||||
use crate::IoError;
|
||||
use crate::MaybeApplyPatchVerified;
|
||||
use crate::parser::Hunk;
|
||||
use crate::parser::ParseError;
|
||||
use crate::parser::parse_patch;
|
||||
use crate::unified_diff_from_chunks;
|
||||
use super::parser::Hunk;
|
||||
use super::parser::ParseError;
|
||||
use super::parser::parse_patch;
|
||||
use std::str::Utf8Error;
|
||||
use tree_sitter::LanguageError;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1074
codex-rs/apply-patch/src/native.rs
Normal file
1074
codex-rs/apply-patch/src/native.rs
Normal file
File diff suppressed because it is too large
Load Diff
205
codex-rs/apply-patch/src/wasm.rs
Normal file
205
codex-rs/apply-patch/src/wasm.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_tool_instructions.md");
|
||||
pub const CODEX_CORE_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch";
|
||||
|
||||
const APPLY_PATCH_UNAVAILABLE: &str = "apply_patch is unavailable on wasm32";
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Error)]
|
||||
pub enum ParseError {
|
||||
#[error("invalid patch: {0}")]
|
||||
InvalidPatchError(String),
|
||||
#[error("invalid hunk at line {line_number}, {message}")]
|
||||
InvalidHunkError { message: String, line_number: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum Hunk {
|
||||
AddFile {
|
||||
path: PathBuf,
|
||||
contents: String,
|
||||
},
|
||||
DeleteFile {
|
||||
path: PathBuf,
|
||||
},
|
||||
UpdateFile {
|
||||
path: PathBuf,
|
||||
move_path: Option<PathBuf>,
|
||||
chunks: Vec<UpdateFileChunk>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct UpdateFileChunk {
|
||||
pub change_context: Option<String>,
|
||||
pub old_lines: Vec<String>,
|
||||
pub new_lines: Vec<String>,
|
||||
pub is_end_of_file: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{context}: {source}")]
|
||||
pub struct IoError {
|
||||
context: String,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
}
|
||||
|
||||
impl PartialEq for IoError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.context == other.context && self.source.to_string() == other.source.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum ApplyPatchError {
|
||||
#[error(transparent)]
|
||||
ParseError(#[from] ParseError),
|
||||
#[error(transparent)]
|
||||
IoError(#[from] IoError),
|
||||
#[error("{0}")]
|
||||
ComputeReplacements(String),
|
||||
#[error(
|
||||
"patch detected without explicit call to apply_patch. Rerun as [\"apply_patch\", \"<patch>\"]"
|
||||
)]
|
||||
ImplicitInvocation,
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ApplyPatchError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Self::IoError(IoError {
|
||||
context: "I/O error".to_string(),
|
||||
source: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&std::io::Error> for ApplyPatchError {
|
||||
fn from(err: &std::io::Error) -> Self {
|
||||
Self::IoError(IoError {
|
||||
context: "I/O error".to_string(),
|
||||
source: std::io::Error::new(err.kind(), err.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ApplyPatchArgs {
|
||||
pub patch: String,
|
||||
pub hunks: Vec<Hunk>,
|
||||
pub workdir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ApplyPatchFileChange {
|
||||
Add {
|
||||
content: String,
|
||||
},
|
||||
Delete {
|
||||
content: String,
|
||||
},
|
||||
Update {
|
||||
unified_diff: String,
|
||||
move_path: Option<PathBuf>,
|
||||
new_content: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ExtractHeredocError {
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExtractHeredocError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{APPLY_PATCH_UNAVAILABLE}")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ExtractHeredocError {}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum MaybeApplyPatchVerified {
|
||||
Body(ApplyPatchAction),
|
||||
ShellParseError(ExtractHeredocError),
|
||||
CorrectnessError(ApplyPatchError),
|
||||
NotApplyPatch,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ApplyPatchAction {
|
||||
changes: HashMap<PathBuf, ApplyPatchFileChange>,
|
||||
pub patch: String,
|
||||
pub cwd: PathBuf,
|
||||
}
|
||||
|
||||
impl ApplyPatchAction {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.changes.is_empty()
|
||||
}
|
||||
|
||||
pub fn changes(&self) -> &HashMap<PathBuf, ApplyPatchFileChange> {
|
||||
&self.changes
|
||||
}
|
||||
|
||||
pub fn new_add_for_test(path: &Path, content: String) -> Self {
|
||||
let changes = HashMap::from([(path.to_path_buf(), ApplyPatchFileChange::Add { content })]);
|
||||
Self {
|
||||
changes,
|
||||
patch: String::new(),
|
||||
cwd: path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.to_path_buf(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ApplyPatchFileUpdate {
|
||||
pub unified_diff: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub fn parse_patch(_patch: &str) -> Result<ApplyPatchArgs, ParseError> {
|
||||
Err(ParseError::InvalidPatchError(
|
||||
APPLY_PATCH_UNAVAILABLE.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn unified_diff_from_chunks(
|
||||
_path: &Path,
|
||||
_chunks: &[UpdateFileChunk],
|
||||
) -> Result<ApplyPatchFileUpdate, ApplyPatchError> {
|
||||
Err(ApplyPatchError::ComputeReplacements(
|
||||
APPLY_PATCH_UNAVAILABLE.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn maybe_parse_apply_patch_verified(argv: &[String], _cwd: &Path) -> MaybeApplyPatchVerified {
|
||||
match argv.first().map(String::as_str) {
|
||||
Some("apply_patch" | "applypatch") => MaybeApplyPatchVerified::CorrectnessError(
|
||||
ApplyPatchError::ComputeReplacements(APPLY_PATCH_UNAVAILABLE.to_string()),
|
||||
),
|
||||
_ => MaybeApplyPatchVerified::NotApplyPatch,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_patch(
|
||||
_patch: &str,
|
||||
_stdout: &mut impl std::io::Write,
|
||||
_stderr: &mut impl std::io::Write,
|
||||
) -> Result<(), ApplyPatchError> {
|
||||
Err(ApplyPatchError::ComputeReplacements(
|
||||
APPLY_PATCH_UNAVAILABLE.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn main() -> ! {
|
||||
panic!("{APPLY_PATCH_UNAVAILABLE}");
|
||||
}
|
||||
@@ -9,8 +9,13 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "time"] }
|
||||
tokio-util.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "time"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -7,13 +7,15 @@ pub enum CancelErr {
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
pub trait OrCancelExt: Sized {
|
||||
type Output;
|
||||
|
||||
async fn or_cancel(self, token: &CancellationToken) -> Result<Self::Output, CancelErr>;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl<F> OrCancelExt for F
|
||||
where
|
||||
@@ -30,6 +32,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl<F> OrCancelExt for F
|
||||
where
|
||||
F: Future,
|
||||
{
|
||||
type Output = F::Output;
|
||||
|
||||
async fn or_cancel(self, token: &CancellationToken) -> Result<Self::Output, CancelErr> {
|
||||
tokio::select! {
|
||||
_ = token.cancelled() => Err(CancelErr::Cancelled),
|
||||
res = self => Ok(res),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -16,10 +16,13 @@ workspace = true
|
||||
async-trait = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
|
||||
tokio-util = { workspace = true, features = ["rt"] }
|
||||
tracing = { workspace = true }
|
||||
v8 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
v8 = { workspace = true }
|
||||
|
||||
@@ -9,7 +9,7 @@ const CODE_MODE_ONLY_PREFACE: &str =
|
||||
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly";
|
||||
const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/compose tool calls
|
||||
- Evaluates the provided JavaScript code in a fresh V8 isolate as an async module.
|
||||
- All nested tools are available on the global `tools` object, for example `await tools.exec_command(...)`. Tool names are exposed as normalized JavaScript identifiers, for example `await tools.mcp__ologs__get_profile(...)`.
|
||||
- If nested tools are enabled for this session, they are available on the global `tools` object, for example `await tools.exec_command(...)`. Tool names are exposed as normalized JavaScript identifiers, for example `await tools.mcp__ologs__get_profile(...)`.
|
||||
- Nested tool methods take either a string or an object as their input argument.
|
||||
- Nested tools return either an object or a string, based on the description.
|
||||
- Runs raw JavaScript -- no Node, no file system, no network access, no console.
|
||||
@@ -89,6 +89,7 @@ pub fn parse_exec_source(input: &str) -> Result<ParsedExecSource, String> {
|
||||
let rest = lines.next().unwrap_or_default();
|
||||
let trimmed = first_line.trim_start();
|
||||
let Some(pragma) = trimmed.strip_prefix(CODE_MODE_PRAGMA_PREFIX) else {
|
||||
args.code = normalize_exec_source(&args.code)?;
|
||||
return Ok(args);
|
||||
};
|
||||
|
||||
@@ -149,12 +150,58 @@ pub fn parse_exec_source(input: &str) -> Result<ParsedExecSource, String> {
|
||||
);
|
||||
}
|
||||
|
||||
args.code = rest.to_string();
|
||||
args.code = normalize_exec_source(rest)?;
|
||||
args.yield_time_ms = pragma.yield_time_ms;
|
||||
args.max_output_tokens = pragma.max_output_tokens;
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn normalize_exec_source(code: &str) -> Result<String, String> {
|
||||
let trimmed = code.trim();
|
||||
if trimmed.starts_with("```") {
|
||||
return Err(
|
||||
"exec expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// @exec: ...`).".to_string(),
|
||||
);
|
||||
}
|
||||
let Ok(value) = serde_json::from_str::<JsonValue>(trimmed) else {
|
||||
return Ok(code.to_string());
|
||||
};
|
||||
match value {
|
||||
JsonValue::Object(object) => {
|
||||
if object.len() == 1
|
||||
&& let Some(JsonValue::String(source)) = object.get("code")
|
||||
{
|
||||
return Ok(source.clone());
|
||||
}
|
||||
if let Some(command) = object
|
||||
.get("command")
|
||||
.or_else(|| object.get("cmd"))
|
||||
.and_then(JsonValue::as_str)
|
||||
&& let Some(source) = extract_node_dash_e_source(command)
|
||||
{
|
||||
return Ok(source);
|
||||
}
|
||||
Err(
|
||||
"exec is a freeform tool and expects raw JavaScript source. Resend plain JS only (optional first line `// @exec: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences.".to_string(),
|
||||
)
|
||||
}
|
||||
JsonValue::String(_) => Err(
|
||||
"exec is a freeform tool and expects raw JavaScript source. Resend plain JS only (optional first line `// @exec: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences.".to_string(),
|
||||
),
|
||||
_ => Ok(code.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_node_dash_e_source(command: &str) -> Option<String> {
|
||||
let parts = shlex::split(command)?;
|
||||
match parts.as_slice() {
|
||||
[program, flag, source] if program == "node" && (flag == "-e" || flag == "--eval") => {
|
||||
Some(source.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_code_mode_nested_tool(tool_name: &str) -> bool {
|
||||
tool_name != crate::PUBLIC_TOOL_NAME && tool_name != crate::WAIT_TOOL_NAME
|
||||
}
|
||||
@@ -185,6 +232,8 @@ pub fn build_exec_tool_description(
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
sections.push(nested_tool_reference);
|
||||
} else {
|
||||
sections.push("No nested tools are enabled for this session. Use plain JavaScript together with the built-in helpers such as `text(...)`.".to_string());
|
||||
}
|
||||
|
||||
sections.join("\n\n")
|
||||
@@ -225,6 +274,7 @@ pub fn augment_tool_definition(mut definition: ToolDefinition) -> ToolDefinition
|
||||
definition
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn enabled_tool_metadata(definition: &ToolDefinition) -> EnabledToolMetadata {
|
||||
EnabledToolMetadata {
|
||||
tool_name: definition.name.clone(),
|
||||
@@ -234,6 +284,7 @@ pub fn enabled_tool_metadata(definition: &ToolDefinition) -> EnabledToolMetadata
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct EnabledToolMetadata {
|
||||
pub tool_name: String,
|
||||
@@ -508,6 +559,51 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_exec_source_unwraps_code_json_object() {
|
||||
let parsed = parse_exec_source("{\"code\":\"text('hi')\"}").expect("expected parse");
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ParsedExecSource {
|
||||
code: "text('hi')".to_string(),
|
||||
yield_time_ms: None,
|
||||
max_output_tokens: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_exec_source_unwraps_node_dash_e_command_object() {
|
||||
let parsed = parse_exec_source("{\"command\":\"node -e \\\"text('hi')\\\"\"}")
|
||||
.expect("expected parse");
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ParsedExecSource {
|
||||
code: "text('hi')".to_string(),
|
||||
yield_time_ms: None,
|
||||
max_output_tokens: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_exec_source_rejects_non_code_json_object_input() {
|
||||
let err = parse_exec_source("{\"command\":\"which node\"}").expect_err("expected error");
|
||||
assert_eq!(
|
||||
err,
|
||||
"exec is a freeform tool and expects raw JavaScript source. Resend plain JS only (optional first line `// @exec: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_exec_source_rejects_markdown_fences() {
|
||||
let err = parse_exec_source("```js\ntext('hi')\n```").expect_err("expected error");
|
||||
assert_eq!(
|
||||
err,
|
||||
"exec expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// @exec: ...`)."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_identifier_rewrites_invalid_characters() {
|
||||
assert_eq!(
|
||||
@@ -556,4 +652,10 @@ mod tests {
|
||||
);
|
||||
assert!(description.contains("### `foo` (`foo`)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_only_description_mentions_absent_nested_tools() {
|
||||
let description = build_exec_tool_description(&[], /*code_mode_only*/ true);
|
||||
assert!(description.contains("No nested tools are enabled for this session"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod description;
|
||||
mod response;
|
||||
mod runtime;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "service_wasm.rs")]
|
||||
mod service;
|
||||
|
||||
pub use description::CODE_MODE_PRAGMA_PREFIX;
|
||||
@@ -22,9 +23,11 @@ pub use runtime::DEFAULT_WAIT_YIELD_TIME_MS;
|
||||
pub use runtime::ExecuteRequest;
|
||||
pub use runtime::RuntimeResponse;
|
||||
pub use runtime::WaitRequest;
|
||||
pub use service::CodeModeRuntime;
|
||||
pub use service::CodeModeService;
|
||||
pub use service::CodeModeTurnHost;
|
||||
pub use service::CodeModeTurnWorker;
|
||||
pub use service::CodeModeTurnWorkerHandle;
|
||||
|
||||
pub const PUBLIC_TOOL_NAME: &str = "exec";
|
||||
pub const WAIT_TOOL_NAME: &str = "wait";
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod callbacks;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod globals;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod module_loader;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod value;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::mpsc as std_mpsc;
|
||||
use std::thread;
|
||||
|
||||
use serde_json::Value as JsonValue;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::description::EnabledToolMetadata;
|
||||
use crate::description::ToolDefinition;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::description::enabled_tool_metadata;
|
||||
use crate::response::FunctionCallOutputContentItem;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::sync::OnceLock;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::sync::mpsc as std_mpsc;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::thread;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000;
|
||||
pub const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000;
|
||||
pub const DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL: usize = 10_000;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const EXIT_SENTINEL: &str = "__codex_code_mode_exit__";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -56,6 +67,7 @@ pub enum RuntimeResponse {
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum TurnMessage {
|
||||
ToolCall {
|
||||
@@ -71,6 +83,7 @@ pub(crate) enum TurnMessage {
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RuntimeCommand {
|
||||
ToolResponse { id: String, result: JsonValue },
|
||||
@@ -78,6 +91,7 @@ pub(crate) enum RuntimeCommand {
|
||||
Terminate,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RuntimeEvent {
|
||||
Started,
|
||||
@@ -98,6 +112,7 @@ pub(crate) enum RuntimeEvent {
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn spawn_runtime(
|
||||
request: ExecuteRequest,
|
||||
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
|
||||
@@ -126,6 +141,7 @@ pub(crate) fn spawn_runtime(
|
||||
Ok((command_tx, isolate_handle))
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone)]
|
||||
struct RuntimeConfig {
|
||||
tool_call_id: String,
|
||||
@@ -134,6 +150,7 @@ struct RuntimeConfig {
|
||||
stored_values: HashMap<String, JsonValue>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) struct RuntimeState {
|
||||
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
|
||||
pending_tool_calls: HashMap<String, v8::Global<v8::PromiseResolver>>,
|
||||
@@ -144,6 +161,7 @@ pub(super) struct RuntimeState {
|
||||
exit_requested: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) enum CompletionState {
|
||||
Pending,
|
||||
Completed {
|
||||
@@ -152,6 +170,7 @@ pub(super) enum CompletionState {
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn initialize_v8() {
|
||||
static PLATFORM: OnceLock<v8::SharedRef<v8::Platform>> = OnceLock::new();
|
||||
|
||||
@@ -163,6 +182,7 @@ fn initialize_v8() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn run_runtime(
|
||||
config: RuntimeConfig,
|
||||
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
|
||||
@@ -264,6 +284,7 @@ fn run_runtime(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn capture_scope_send_error(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
event_tx: &mpsc::UnboundedSender<RuntimeEvent>,
|
||||
@@ -277,6 +298,7 @@ fn capture_scope_send_error(
|
||||
send_result(event_tx, stored_values, error_text);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn send_result(
|
||||
event_tx: &mpsc::UnboundedSender<RuntimeEvent>,
|
||||
stored_values: HashMap<String, JsonValue>,
|
||||
@@ -288,7 +310,7 @@ fn send_result(
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -34,6 +34,22 @@ pub trait CodeModeTurnHost: Send + Sync {
|
||||
async fn notify(&self, call_id: String, cell_id: String, text: String) -> Result<(), String>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait CodeModeRuntime: Send + Sync {
|
||||
async fn stored_values(&self) -> HashMap<String, JsonValue>;
|
||||
|
||||
async fn replace_stored_values(&self, values: HashMap<String, JsonValue>);
|
||||
|
||||
async fn execute(&self, request: ExecuteRequest) -> Result<RuntimeResponse, String>;
|
||||
|
||||
async fn wait(&self, request: WaitRequest) -> Result<RuntimeResponse, String>;
|
||||
|
||||
fn start_turn_worker(
|
||||
&self,
|
||||
host: Arc<dyn CodeModeTurnHost>,
|
||||
) -> Box<dyn CodeModeTurnWorkerHandle>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SessionHandle {
|
||||
control_tx: mpsc::UnboundedSender<SessionControlCommand>,
|
||||
@@ -219,6 +235,10 @@ pub struct CodeModeTurnWorker {
|
||||
shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
pub trait CodeModeTurnWorkerHandle: Send {}
|
||||
|
||||
impl CodeModeTurnWorkerHandle for CodeModeTurnWorker {}
|
||||
|
||||
impl Drop for CodeModeTurnWorker {
|
||||
fn drop(&mut self) {
|
||||
if let Some(shutdown_tx) = self.shutdown_tx.take() {
|
||||
@@ -227,6 +247,32 @@ impl Drop for CodeModeTurnWorker {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CodeModeRuntime for CodeModeService {
|
||||
async fn stored_values(&self) -> HashMap<String, JsonValue> {
|
||||
CodeModeService::stored_values(self).await
|
||||
}
|
||||
|
||||
async fn replace_stored_values(&self, values: HashMap<String, JsonValue>) {
|
||||
CodeModeService::replace_stored_values(self, values).await;
|
||||
}
|
||||
|
||||
async fn execute(&self, request: ExecuteRequest) -> Result<RuntimeResponse, String> {
|
||||
CodeModeService::execute(self, request).await
|
||||
}
|
||||
|
||||
async fn wait(&self, request: WaitRequest) -> Result<RuntimeResponse, String> {
|
||||
CodeModeService::wait(self, request).await
|
||||
}
|
||||
|
||||
fn start_turn_worker(
|
||||
&self,
|
||||
host: Arc<dyn CodeModeTurnHost>,
|
||||
) -> Box<dyn CodeModeTurnWorkerHandle> {
|
||||
Box::new(CodeModeService::start_turn_worker(self, host))
|
||||
}
|
||||
}
|
||||
|
||||
enum SessionControlCommand {
|
||||
Poll {
|
||||
yield_time_ms: u64,
|
||||
|
||||
122
codex-rs/code-mode/src/service_wasm.rs
Normal file
122
codex-rs/code-mode/src/service_wasm.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value as JsonValue;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::ExecuteRequest;
|
||||
use crate::RuntimeResponse;
|
||||
use crate::WaitRequest;
|
||||
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
pub trait CodeModeTurnHost: Send + Sync {
|
||||
async fn invoke_tool(
|
||||
&self,
|
||||
tool_name: String,
|
||||
input: Option<JsonValue>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<JsonValue, String>;
|
||||
|
||||
async fn notify(&self, call_id: String, cell_id: String, text: String) -> Result<(), String>;
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
pub trait CodeModeRuntime: Send + Sync {
|
||||
async fn stored_values(&self) -> HashMap<String, JsonValue>;
|
||||
|
||||
async fn replace_stored_values(&self, values: HashMap<String, JsonValue>);
|
||||
|
||||
async fn execute(&self, request: ExecuteRequest) -> Result<RuntimeResponse, String>;
|
||||
|
||||
async fn wait(&self, request: WaitRequest) -> Result<RuntimeResponse, String>;
|
||||
|
||||
fn start_turn_worker(
|
||||
&self,
|
||||
host: Arc<dyn CodeModeTurnHost>,
|
||||
) -> Box<dyn CodeModeTurnWorkerHandle>;
|
||||
}
|
||||
|
||||
pub struct CodeModeService {
|
||||
stored_values: Mutex<HashMap<String, JsonValue>>,
|
||||
}
|
||||
|
||||
impl CodeModeService {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stored_values: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stored_values(&self) -> HashMap<String, JsonValue> {
|
||||
self.stored_values.lock().await.clone()
|
||||
}
|
||||
|
||||
pub async fn replace_stored_values(&self, values: HashMap<String, JsonValue>) {
|
||||
*self.stored_values.lock().await = values;
|
||||
}
|
||||
|
||||
pub async fn execute(&self, _request: ExecuteRequest) -> Result<RuntimeResponse, String> {
|
||||
Err(
|
||||
"native code mode runtime is unavailable on wasm32; inject a browser CodeModeRuntime"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn wait(&self, request: WaitRequest) -> Result<RuntimeResponse, String> {
|
||||
Ok(RuntimeResponse::Result {
|
||||
cell_id: request.cell_id,
|
||||
content_items: Vec::new(),
|
||||
stored_values: self.stored_values().await,
|
||||
error_text: Some(
|
||||
"code mode wait is unavailable on wasm32 without an injected runtime".to_string(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_turn_worker(&self, _host: Arc<dyn CodeModeTurnHost>) -> CodeModeTurnWorker {
|
||||
CodeModeTurnWorker
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CodeModeService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeModeTurnWorker;
|
||||
|
||||
pub trait CodeModeTurnWorkerHandle: Send {}
|
||||
|
||||
impl CodeModeTurnWorkerHandle for CodeModeTurnWorker {}
|
||||
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl CodeModeRuntime for CodeModeService {
|
||||
async fn stored_values(&self) -> HashMap<String, JsonValue> {
|
||||
CodeModeService::stored_values(self).await
|
||||
}
|
||||
|
||||
async fn replace_stored_values(&self, values: HashMap<String, JsonValue>) {
|
||||
CodeModeService::replace_stored_values(self, values).await;
|
||||
}
|
||||
|
||||
async fn execute(&self, request: ExecuteRequest) -> Result<RuntimeResponse, String> {
|
||||
CodeModeService::execute(self, request).await
|
||||
}
|
||||
|
||||
async fn wait(&self, request: WaitRequest) -> Result<RuntimeResponse, String> {
|
||||
CodeModeService::wait(self, request).await
|
||||
}
|
||||
|
||||
fn start_turn_worker(
|
||||
&self,
|
||||
host: Arc<dyn CodeModeTurnHost>,
|
||||
) -> Box<dyn CodeModeTurnWorkerHandle> {
|
||||
Box::new(CodeModeService::start_turn_worker(self, host))
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,12 @@ async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-rustls-provider = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
http = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tungstenite = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
|
||||
tracing = { workspace = true }
|
||||
eventsource-stream = { workspace = true }
|
||||
regex-lite = { workspace = true }
|
||||
@@ -32,5 +29,14 @@ tokio-test = { workspace = true }
|
||||
wiremock = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
codex-utils-rustls-provider = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tungstenite = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod compact;
|
||||
pub mod memories;
|
||||
pub mod models;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod realtime_websocket;
|
||||
pub mod responses;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod responses_websocket;
|
||||
mod session;
|
||||
|
||||
@@ -30,20 +30,30 @@ pub use crate::common::response_create_client_metadata;
|
||||
pub use crate::endpoint::compact::CompactClient;
|
||||
pub use crate::endpoint::memories::MemoriesClient;
|
||||
pub use crate::endpoint::models::ModelsClient;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::endpoint::realtime_websocket::RealtimeEventParser;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::endpoint::realtime_websocket::RealtimeSessionConfig;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::endpoint::realtime_websocket::RealtimeSessionMode;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::endpoint::realtime_websocket::RealtimeWebsocketClient;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::endpoint::realtime_websocket::RealtimeWebsocketConnection;
|
||||
pub use crate::endpoint::responses::ResponsesClient;
|
||||
pub use crate::endpoint::responses::ResponsesOptions;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::endpoint::responses_websocket::ResponsesWebsocketClient;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::endpoint::responses_websocket::ResponsesWebsocketConnection;
|
||||
pub use crate::error::ApiError;
|
||||
pub use crate::provider::Provider;
|
||||
pub use crate::provider::is_azure_responses_wire_base_url;
|
||||
pub use crate::sse::stream_from_fixture;
|
||||
pub use crate::telemetry::SseTelemetry;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::telemetry::WebsocketTelemetry;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_protocol::protocol::RealtimeAudioFrame;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_protocol::protocol::RealtimeEvent;
|
||||
|
||||
@@ -19,16 +19,22 @@ use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::Instant;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::timeout;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tracing::debug;
|
||||
use tracing::trace;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
const X_REASONING_INCLUDED_HEADER: &str = "x-reasoning-included";
|
||||
const OPENAI_MODEL_HEADER: &str = "openai-model";
|
||||
|
||||
/// Streams SSE events from an on-disk fixture for tests.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn stream_from_fixture(
|
||||
path: impl AsRef<Path>,
|
||||
idle_timeout: Duration,
|
||||
@@ -45,7 +51,7 @@ pub fn stream_from_fixture(
|
||||
let reader = std::io::Cursor::new(content);
|
||||
let stream = ReaderStream::new(reader).map_err(|err| TransportError::Network(err.to_string()));
|
||||
let (tx_event, rx_event) = mpsc::channel::<Result<ResponseEvent, ApiError>>(1600);
|
||||
tokio::spawn(process_sse(
|
||||
spawn_sse_task(process_sse(
|
||||
Box::pin(stream),
|
||||
tx_event,
|
||||
idle_timeout,
|
||||
@@ -54,6 +60,16 @@ pub fn stream_from_fixture(
|
||||
Ok(ResponseStream { rx_event })
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn stream_from_fixture(
|
||||
_path: impl AsRef<Path>,
|
||||
_idle_timeout: Duration,
|
||||
) -> Result<ResponseStream, ApiError> {
|
||||
Err(ApiError::Stream(
|
||||
"SSE fixtures are unavailable on wasm32".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn spawn_response_stream(
|
||||
stream_response: StreamResponse,
|
||||
idle_timeout: Duration,
|
||||
@@ -84,7 +100,7 @@ pub fn spawn_response_stream(
|
||||
let _ = turn_state.set(header_value.to_string());
|
||||
}
|
||||
let (tx_event, rx_event) = mpsc::channel::<Result<ResponseEvent, ApiError>>(1600);
|
||||
tokio::spawn(async move {
|
||||
spawn_sse_task(async move {
|
||||
if let Some(model) = server_model {
|
||||
let _ = tx_event.send(Ok(ResponseEvent::ServerModel(model))).await;
|
||||
}
|
||||
@@ -105,6 +121,73 @@ pub fn spawn_response_stream(
|
||||
ResponseStream { rx_event }
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn spawn_sse_task(task: impl std::future::Future<Output = ()> + Send + 'static) {
|
||||
tokio::spawn(task);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn spawn_sse_task(task: impl std::future::Future<Output = ()> + 'static) {
|
||||
spawn_local(task);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn next_sse_event<S>(
|
||||
stream: &mut S,
|
||||
idle_timeout: Duration,
|
||||
) -> Result<
|
||||
Option<Result<eventsource_stream::Event, eventsource_stream::EventStreamError<TransportError>>>,
|
||||
tokio::time::error::Elapsed,
|
||||
>
|
||||
where
|
||||
S: futures::Stream<
|
||||
Item = Result<
|
||||
eventsource_stream::Event,
|
||||
eventsource_stream::EventStreamError<TransportError>,
|
||||
>,
|
||||
> + Unpin,
|
||||
{
|
||||
timeout(idle_timeout, stream.next()).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn next_sse_event<S>(
|
||||
stream: &mut S,
|
||||
idle_timeout: Duration,
|
||||
) -> Result<
|
||||
Option<Result<eventsource_stream::Event, eventsource_stream::EventStreamError<TransportError>>>,
|
||||
tokio::time::error::Elapsed,
|
||||
>
|
||||
where
|
||||
S: futures::Stream<
|
||||
Item = Result<
|
||||
eventsource_stream::Event,
|
||||
eventsource_stream::EventStreamError<TransportError>,
|
||||
>,
|
||||
> + Unpin,
|
||||
{
|
||||
let _ = idle_timeout;
|
||||
Ok(stream.next().await)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn sse_poll_start() -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn sse_poll_start() {}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn sse_poll_duration_since(start: Instant) -> Duration {
|
||||
start.elapsed()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn sse_poll_duration_since((): ()) -> Duration {
|
||||
Duration::ZERO
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Error {
|
||||
@@ -365,10 +448,10 @@ pub async fn process_sse(
|
||||
let mut last_server_model: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
let response = timeout(idle_timeout, stream.next()).await;
|
||||
let start = sse_poll_start();
|
||||
let response = next_sse_event(&mut stream, idle_timeout).await;
|
||||
if let Some(t) = telemetry.as_ref() {
|
||||
t.on_sse_poll(&response, start.elapsed());
|
||||
t.on_sse_poll(&response, sse_poll_duration_since(start));
|
||||
}
|
||||
let sse = match response {
|
||||
Ok(Some(Ok(sse))) => sse,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::error::ApiError;
|
||||
use codex_client::Request;
|
||||
use codex_client::RequestTelemetry;
|
||||
@@ -10,8 +11,11 @@ use http::StatusCode;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::Instant;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::tungstenite::Error;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
/// Generic telemetry.
|
||||
@@ -31,6 +35,7 @@ pub trait SseTelemetry: Send + Sync {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Telemetry for Responses WebSocket transport.
|
||||
pub trait WebsocketTelemetry: Send + Sync {
|
||||
fn on_ws_request(&self, duration: Duration, error: Option<&ApiError>, connection_reused: bool);
|
||||
@@ -53,6 +58,24 @@ fn http_status(err: &TransportError) -> Option<StatusCode> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn request_start_time() -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn request_start_time() {}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn request_duration_since(start: Instant) -> Duration {
|
||||
start.elapsed()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn request_duration_since((): ()) -> Duration {
|
||||
Duration::ZERO
|
||||
}
|
||||
|
||||
impl WithStatus for Response {
|
||||
fn status(&self) -> StatusCode {
|
||||
self.status
|
||||
@@ -82,14 +105,14 @@ where
|
||||
let telemetry = telemetry.clone();
|
||||
let send = send.clone();
|
||||
async move {
|
||||
let start = Instant::now();
|
||||
let start = request_start_time();
|
||||
let result = send(req).await;
|
||||
if let Some(t) = telemetry.as_ref() {
|
||||
let (status, err) = match &result {
|
||||
Ok(resp) => (Some(resp.status()), None),
|
||||
Err(err) => (http_status(err), Some(err)),
|
||||
};
|
||||
t.on_request(attempt, status, err, start.elapsed());
|
||||
t.on_request(attempt, status, err, request_duration_since(start));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -11,20 +11,25 @@ eventsource-stream = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
http = { workspace = true }
|
||||
opentelemetry = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "stream"] }
|
||||
rustls = { workspace = true }
|
||||
rustls-native-certs = { workspace = true }
|
||||
rustls-pki-types = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time", "sync"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-opentelemetry = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
codex-utils-rustls-provider = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
rustls-native-certs = { workspace = true }
|
||||
rustls-pki-types = { workspace = true }
|
||||
zstd = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
37
codex-rs/codex-client/src/custom_ca_wasm.rs
Normal file
37
codex-rs/codex-client/src/custom_ca_wasm.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::io;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub const CODEX_CA_CERT_ENV: &str = "CODEX_CA_CERTIFICATE";
|
||||
pub const SSL_CERT_FILE_ENV: &str = "SSL_CERT_FILE";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BuildCustomCaTransportError {
|
||||
#[error("failed to build HTTP client while using browser transport defaults: {0}")]
|
||||
BuildClientWithSystemRoots(#[source] reqwest::Error),
|
||||
}
|
||||
|
||||
impl From<BuildCustomCaTransportError> for io::Error {
|
||||
fn from(error: BuildCustomCaTransportError) -> Self {
|
||||
io::Error::other(error)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_reqwest_client_with_custom_ca(
|
||||
builder: reqwest::ClientBuilder,
|
||||
) -> Result<reqwest::Client, BuildCustomCaTransportError> {
|
||||
builder
|
||||
.build()
|
||||
.map_err(BuildCustomCaTransportError::BuildClientWithSystemRoots)
|
||||
}
|
||||
|
||||
pub fn build_reqwest_client_for_subprocess_tests(
|
||||
builder: reqwest::ClientBuilder,
|
||||
) -> Result<reqwest::Client, BuildCustomCaTransportError> {
|
||||
build_reqwest_client_with_custom_ca(builder)
|
||||
}
|
||||
|
||||
pub fn maybe_build_rustls_client_config_with_custom_ca()
|
||||
-> Result<Option<()>, BuildCustomCaTransportError> {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -115,6 +115,7 @@ impl CodexRequestBuilder {
|
||||
|
||||
match self.builder.headers(headers).send().await {
|
||||
Ok(response) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
tracing::debug!(
|
||||
method = %self.method,
|
||||
url = %self.url,
|
||||
@@ -123,6 +124,14 @@ impl CodexRequestBuilder {
|
||||
version = ?response.version(),
|
||||
"Request completed"
|
||||
);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
tracing::debug!(
|
||||
method = %self.method,
|
||||
url = %self.url,
|
||||
status = %response.status(),
|
||||
headers = ?response.headers(),
|
||||
"Request completed"
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg_attr(target_arch = "wasm32", path = "custom_ca_wasm.rs")]
|
||||
mod custom_ca;
|
||||
mod default_client;
|
||||
mod error;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::error::TransportError;
|
||||
use crate::request::Request;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use rand::Rng;
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
@@ -42,10 +43,20 @@ pub fn backoff(base: Duration, attempt: u64) -> Duration {
|
||||
let exp = 2u64.saturating_pow(attempt as u32 - 1);
|
||||
let millis = base.as_millis() as u64;
|
||||
let raw = millis.saturating_mul(exp);
|
||||
let jitter: f64 = rand::rng().random_range(0.9..1.1);
|
||||
let jitter = jitter_multiplier();
|
||||
Duration::from_millis((raw as f64 * jitter) as u64)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn jitter_multiplier() -> f64 {
|
||||
rand::rng().random_range(0.9..1.1)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn jitter_multiplier() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
pub async fn run_with_retry<T, F, Fut>(
|
||||
policy: RetryPolicy,
|
||||
mut make_req: impl FnMut() -> Request,
|
||||
|
||||
@@ -4,7 +4,10 @@ use eventsource_stream::Eventsource;
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::Duration;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::timeout;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
/// Minimal SSE helper that forwards raw `data:` frames as UTF-8 strings.
|
||||
///
|
||||
@@ -14,13 +17,13 @@ pub fn sse_stream(
|
||||
idle_timeout: Duration,
|
||||
tx: mpsc::Sender<Result<String, StreamError>>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
spawn_sse_task(async move {
|
||||
let mut stream = stream
|
||||
.map(|res| res.map_err(|e| StreamError::Stream(e.to_string())))
|
||||
.eventsource();
|
||||
|
||||
loop {
|
||||
match timeout(idle_timeout, stream.next()).await {
|
||||
match next_sse_event(&mut stream, idle_timeout).await {
|
||||
Ok(Some(Ok(ev))) => {
|
||||
if tx.send(Ok(ev.data.clone())).await.is_err() {
|
||||
return;
|
||||
@@ -46,3 +49,46 @@ pub fn sse_stream(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn next_sse_event<S, E>(
|
||||
stream: &mut S,
|
||||
idle_timeout: Duration,
|
||||
) -> Result<
|
||||
Option<Result<eventsource_stream::Event, eventsource_stream::EventStreamError<E>>>,
|
||||
tokio::time::error::Elapsed,
|
||||
>
|
||||
where
|
||||
S: futures::Stream<
|
||||
Item = Result<eventsource_stream::Event, eventsource_stream::EventStreamError<E>>,
|
||||
> + Unpin,
|
||||
{
|
||||
timeout(idle_timeout, stream.next()).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn next_sse_event<S, E>(
|
||||
stream: &mut S,
|
||||
idle_timeout: Duration,
|
||||
) -> Result<
|
||||
Option<Result<eventsource_stream::Event, eventsource_stream::EventStreamError<E>>>,
|
||||
tokio::time::error::Elapsed,
|
||||
>
|
||||
where
|
||||
S: futures::Stream<
|
||||
Item = Result<eventsource_stream::Event, eventsource_stream::EventStreamError<E>>,
|
||||
> + Unpin,
|
||||
{
|
||||
let _ = idle_timeout;
|
||||
Ok(stream.next().await)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn spawn_sse_task(task: impl std::future::Future<Output = ()> + Send + 'static) {
|
||||
tokio::spawn(task);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn spawn_sse_task(task: impl std::future::Future<Output = ()> + 'static) {
|
||||
spawn_local(task);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ use crate::request::Response;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use futures::stream::BoxStream;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use futures::stream::LocalBoxStream;
|
||||
use http::HeaderMap;
|
||||
use http::Method;
|
||||
use http::StatusCode;
|
||||
@@ -15,15 +18,20 @@ use tracing::Level;
|
||||
use tracing::enabled;
|
||||
use tracing::trace;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type ByteStream = BoxStream<'static, Result<Bytes, TransportError>>;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type ByteStream = LocalBoxStream<'static, Result<Bytes, TransportError>>;
|
||||
|
||||
pub struct StreamResponse {
|
||||
pub status: StatusCode,
|
||||
pub headers: HeaderMap,
|
||||
pub bytes: ByteStream,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait HttpTransport: Send + Sync {
|
||||
async fn execute(&self, req: Request) -> Result<Response, TransportError>;
|
||||
async fn stream(&self, req: Request) -> Result<StreamResponse, TransportError>;
|
||||
@@ -75,11 +83,7 @@ impl ReqwestTransport {
|
||||
let compression_start = std::time::Instant::now();
|
||||
let (compressed, content_encoding) = match compression {
|
||||
RequestCompression::None => unreachable!("guarded by compression != None"),
|
||||
RequestCompression::Zstd => (
|
||||
zstd::stream::encode_all(std::io::Cursor::new(json), 3)
|
||||
.map_err(|err| TransportError::Build(err.to_string()))?,
|
||||
http::HeaderValue::from_static("zstd"),
|
||||
),
|
||||
RequestCompression::Zstd => encode_zstd(json)?,
|
||||
};
|
||||
let post_compression_bytes = compressed.len();
|
||||
let compression_duration = compression_start.elapsed();
|
||||
@@ -119,7 +123,24 @@ impl ReqwestTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn encode_zstd(json: Vec<u8>) -> Result<(Vec<u8>, http::HeaderValue), TransportError> {
|
||||
Ok((
|
||||
zstd::stream::encode_all(std::io::Cursor::new(json), 3)
|
||||
.map_err(|err| TransportError::Build(err.to_string()))?,
|
||||
http::HeaderValue::from_static("zstd"),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn encode_zstd(_json: Vec<u8>) -> Result<(Vec<u8>, http::HeaderValue), TransportError> {
|
||||
Err(TransportError::Build(
|
||||
"zstd request compression is unavailable on wasm32".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl HttpTransport for ReqwestTransport {
|
||||
async fn execute(&self, req: Request) -> Result<Response, TransportError> {
|
||||
if enabled!(Level::TRACE) {
|
||||
|
||||
@@ -20,11 +20,13 @@ serde_json = { workspace = true }
|
||||
serde_path_to_error = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
toml = { workspace = true }
|
||||
toml_edit = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -19,6 +19,16 @@ use toml_edit::Item;
|
||||
use toml_edit::Table;
|
||||
use toml_edit::Value;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn read_config_contents(path: &Path) -> io::Result<String> {
|
||||
tokio::fs::read_to_string(path).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn read_config_contents(path: &Path) -> io::Result<String> {
|
||||
std::fs::read_to_string(path)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TextPosition {
|
||||
pub line: usize,
|
||||
@@ -169,7 +179,7 @@ where
|
||||
let Some(path) = config_path_for_layer(layer, config_toml_file) else {
|
||||
continue;
|
||||
};
|
||||
let contents = match tokio::fs::read_to_string(&path).await {
|
||||
let contents = match read_config_contents(&path).await {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) => {
|
||||
|
||||
@@ -30,11 +30,16 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "macros", "rt"] }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { workspace = true, features = ["fs", "macros", "rt"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -13,7 +13,16 @@ use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_plugins::mention_syntax::TOOL_MENTION_SIGIL;
|
||||
use tokio::fs;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn read_skill_contents(path: &std::path::Path) -> std::io::Result<String> {
|
||||
tokio::fs::read_to_string(path).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn read_skill_contents(path: &std::path::Path) -> std::io::Result<String> {
|
||||
std::fs::read_to_string(path)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SkillInjections {
|
||||
@@ -38,7 +47,7 @@ pub async fn build_skill_injections(
|
||||
let mut invocations = Vec::new();
|
||||
|
||||
for skill in mentioned_skills {
|
||||
match fs::read_to_string(&skill.path_to_skills_md).await {
|
||||
match read_skill_contents(&skill.path_to_skills_md).await {
|
||||
Ok(contents) => {
|
||||
emit_skill_injected_metric(otel, skill, "ok");
|
||||
invocations.push(SkillInvocation {
|
||||
|
||||
@@ -11,6 +11,38 @@ use codex_login::default_client::build_reqwest_client;
|
||||
|
||||
const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn create_dir_all(path: &Path) -> std::io::Result<()> {
|
||||
tokio::fs::create_dir_all(path).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn create_dir_all(path: &Path) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(path)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn extract_zip_async(
|
||||
zip_bytes: Vec<u8>,
|
||||
output_dir: PathBuf,
|
||||
prefix_candidates: Vec<String>,
|
||||
) -> Result<()> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
extract_zip_to_dir(zip_bytes, &output_dir, &prefix_candidates)
|
||||
})
|
||||
.await
|
||||
.context("Zip extraction task failed")?
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn extract_zip_async(
|
||||
zip_bytes: Vec<u8>,
|
||||
output_dir: PathBuf,
|
||||
prefix_candidates: Vec<String>,
|
||||
) -> Result<()> {
|
||||
extract_zip_to_dir(zip_bytes, &output_dir, &prefix_candidates)
|
||||
}
|
||||
|
||||
// Low-level client for the remote skill API. This is intentionally kept around for
|
||||
// future wiring, but it is not used yet by any active product surface.
|
||||
|
||||
@@ -182,18 +214,14 @@ pub async fn export_remote_skill(
|
||||
}
|
||||
|
||||
let output_dir = codex_home.join("skills").join(skill_id);
|
||||
tokio::fs::create_dir_all(&output_dir)
|
||||
create_dir_all(&output_dir)
|
||||
.await
|
||||
.context("Failed to create downloaded skills directory")?;
|
||||
|
||||
let zip_bytes = body.to_vec();
|
||||
let output_dir_clone = output_dir.clone();
|
||||
let prefix_candidates = vec![skill_id.to_string()];
|
||||
tokio::task::spawn_blocking(move || {
|
||||
extract_zip_to_dir(zip_bytes, &output_dir_clone, &prefix_candidates)
|
||||
})
|
||||
.await
|
||||
.context("Zip extraction task failed")??;
|
||||
extract_zip_async(zip_bytes, output_dir_clone, prefix_candidates).await?;
|
||||
|
||||
Ok(RemoteSkillDownloadResult {
|
||||
id: skill_id.to_string(),
|
||||
|
||||
@@ -47,10 +47,8 @@ codex-network-proxy = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-rollout = { workspace = true }
|
||||
codex-rmcp-client = { workspace = true }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-state = { workspace = true }
|
||||
codex-terminal-detection = { workspace = true }
|
||||
codex-tools = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
@@ -81,15 +79,8 @@ indexmap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
regex-lite = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "stream"] }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"base64",
|
||||
"macros",
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
@@ -99,21 +90,13 @@ similar = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
test-log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
|
||||
tokio-util = { workspace = true, features = ["rt"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
toml_edit = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v4", "v5"] }
|
||||
which = { workspace = true }
|
||||
wildmatch = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
@@ -124,6 +107,23 @@ seccompiler = { workspace = true }
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
codex-rollout = { workspace = true }
|
||||
codex-state = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"base64",
|
||||
"macros",
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-std", "process", "rt-multi-thread", "signal"] }
|
||||
which = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { workspace = true }
|
||||
|
||||
# Build OpenSSL from source for musl builds.
|
||||
[target.x86_64-unknown-linux-musl.dependencies]
|
||||
openssl-sys = { workspace = true, features = ["vendored"] }
|
||||
|
||||
@@ -29,6 +29,7 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -352,11 +353,19 @@ impl AgentControl {
|
||||
let Ok(resumed_thread) = state.get_thread(resumed_thread_id).await else {
|
||||
return Ok(resumed_thread_id);
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
return Ok(resumed_thread_id);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let Some(state_db_ctx) = resumed_thread.state_db() else {
|
||||
return Ok(resumed_thread_id);
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut resume_queue = VecDeque::from([(thread_id, root_depth)]);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
while let Some((parent_thread_id, parent_depth)) = resume_queue.pop_front() {
|
||||
let child_ids = match state_db_ctx
|
||||
.list_thread_spawn_children_with_status(
|
||||
@@ -617,6 +626,7 @@ impl AgentControl {
|
||||
/// agent and any live descendants reached from the in-memory tree.
|
||||
pub(crate) async fn close_agent(&self, agent_id: ThreadId) -> CodexResult<String> {
|
||||
let state = self.upgrade()?;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Ok(thread) = state.get_thread(agent_id).await
|
||||
&& let Some(state_db_ctx) = thread.state_db()
|
||||
&& let Err(err) = state_db_ctx
|
||||
@@ -1045,9 +1055,16 @@ impl AgentControl {
|
||||
let Some(parent_thread_id) = session_source.and_then(thread_spawn_parent_thread_id) else {
|
||||
return;
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let Some(state_db_ctx) = thread.state_db() else {
|
||||
return;
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Err(err) = state_db_ctx
|
||||
.upsert_thread_spawn_edge(
|
||||
parent_thread_id,
|
||||
|
||||
@@ -4,7 +4,6 @@ use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::Entry;
|
||||
@@ -13,6 +12,22 @@ use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn choose_item<T>(items: &[T]) -> Option<&T> {
|
||||
use rand::prelude::IndexedRandom;
|
||||
|
||||
items.choose(&mut rand::rng())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn choose_item<T>(items: &[T]) -> Option<&T> {
|
||||
if items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let index = (js_sys::Math::random() * items.len() as f64).floor() as usize;
|
||||
items.get(index.min(items.len().saturating_sub(1)))
|
||||
}
|
||||
|
||||
/// This structure is used to add some limits on the multi-agent capabilities for Codex. In
|
||||
/// the current implementation, it limits:
|
||||
/// * Total number of sub-agents (i.e. threads) per user session
|
||||
@@ -215,7 +230,7 @@ impl AgentRegistry {
|
||||
.map(|name| format_agent_nickname(name, active_agents.nickname_reset_count))
|
||||
.filter(|name| !active_agents.used_agent_nicknames.contains(name))
|
||||
.collect();
|
||||
if let Some(name) = available_names.choose(&mut rand::rng()) {
|
||||
if let Some(name) = choose_item(&available_names) {
|
||||
name.clone()
|
||||
} else {
|
||||
active_agents.used_agent_nicknames.clear();
|
||||
@@ -227,10 +242,7 @@ impl AgentRegistry {
|
||||
&[],
|
||||
);
|
||||
}
|
||||
format_agent_nickname(
|
||||
names.choose(&mut rand::rng())?,
|
||||
active_agents.nickname_reset_count,
|
||||
)
|
||||
format_agent_nickname(choose_item(names)?, active_agents.nickname_reset_count)
|
||||
}
|
||||
};
|
||||
active_agents
|
||||
|
||||
@@ -88,7 +88,7 @@ async fn load_role_layer_toml(
|
||||
let role_config_toml: TomlValue = toml::from_str(&role_config_contents)?;
|
||||
(role_config_toml, config.codex_home.as_path())
|
||||
} else {
|
||||
let role_config_contents = tokio::fs::read_to_string(config_file).await?;
|
||||
let role_config_contents = crate::async_fs::read_to_string(config_file).await?;
|
||||
let role_config_base = config_file
|
||||
.parent()
|
||||
.ok_or(anyhow!("No corresponding config content"))?;
|
||||
|
||||
62
codex-rs/core/src/async_fs.rs
Normal file
62
codex-rs/core/src/async_fs.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
|
||||
tokio::fs::read(path).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
|
||||
std::fs::read(path)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
|
||||
tokio::fs::read_to_string(path).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
|
||||
std::fs::read_to_string(path)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
tokio::fs::write(path, contents).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) async fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
std::fs::write(path, contents)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
|
||||
tokio::fs::create_dir_all(path).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) async fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
|
||||
std::fs::create_dir_all(path)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn metadata(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
|
||||
tokio::fs::metadata(path).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) async fn metadata(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
|
||||
std::fs::metadata(path)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn try_exists(path: impl AsRef<Path>) -> io::Result<bool> {
|
||||
tokio::fs::try_exists(path).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) async fn try_exists(path: impl AsRef<Path>) -> io::Result<bool> {
|
||||
Ok(path.as_ref().exists())
|
||||
}
|
||||
21
codex-rs/core/src/async_runtime.rs
Normal file
21
codex-rs/core/src/async_runtime.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::future::Future;
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn spawn<F>(future: F) -> JoinHandle<F::Output>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
tokio::spawn(future)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) fn spawn<F>(future: F) -> JoinHandle<F::Output>
|
||||
where
|
||||
F: Future + 'static,
|
||||
F::Output: 'static,
|
||||
{
|
||||
tokio::task::spawn_local(future)
|
||||
}
|
||||
@@ -48,10 +48,13 @@ use codex_api::ResponseCreateWsRequest;
|
||||
use codex_api::ResponsesApiRequest;
|
||||
use codex_api::ResponsesClient as ApiResponsesClient;
|
||||
use codex_api::ResponsesOptions as ApiResponsesOptions;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use codex_api::ResponsesWebsocketClient as ApiWebSocketResponsesClient;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use codex_api::ResponsesWebsocketConnection as ApiWebSocketConnection;
|
||||
use codex_api::SseTelemetry;
|
||||
use codex_api::TransportError;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use codex_api::WebsocketTelemetry;
|
||||
use codex_api::build_conversation_headers;
|
||||
use codex_api::common::Reasoning;
|
||||
@@ -84,8 +87,11 @@ use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::sync::oneshot::error::TryRecvError;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::tungstenite::Error;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tracing::instrument;
|
||||
use tracing::trace;
|
||||
@@ -118,6 +124,7 @@ pub const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";
|
||||
pub const X_CODEX_TURN_METADATA_HEADER: &str = "x-codex-turn-metadata";
|
||||
pub const X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER: &str =
|
||||
"x-responsesapi-include-timing-metrics";
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const RESPONSES_WEBSOCKETS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06";
|
||||
const RESPONSES_ENDPOINT: &str = "/responses";
|
||||
const RESPONSES_COMPACT_ENDPOINT: &str = "/responses/compact";
|
||||
@@ -219,6 +226,7 @@ struct LastResponse {
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct WebsocketSession {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
connection: Option<ApiWebSocketConnection>,
|
||||
last_request: Option<ResponsesApiRequest>,
|
||||
last_response_rx: Option<oneshot::Receiver<LastResponse>>,
|
||||
@@ -241,6 +249,7 @@ impl WebsocketSession {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
enum WebsocketStreamOutcome {
|
||||
Stream(ResponseStream),
|
||||
FallbackToHttp,
|
||||
@@ -514,6 +523,15 @@ impl ModelClient {
|
||||
/// Returns whether the Responses-over-WebSocket transport is active for this session.
|
||||
///
|
||||
/// WebSocket use is controlled by provider capability and session-scoped fallback state.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn responses_websocket_enabled(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns whether the Responses-over-WebSocket transport is active for this session.
|
||||
///
|
||||
/// WebSocket use is controlled by provider capability and session-scoped fallback state.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn responses_websocket_enabled(&self) -> bool {
|
||||
if !self.state.provider.supports_websockets
|
||||
|| self.state.disable_websockets.load(Ordering::Relaxed)
|
||||
@@ -551,6 +569,7 @@ impl ModelClient {
|
||||
/// Both startup prewarm and in-turn `needs_new` reconnects call this path so handshake
|
||||
/// behavior remains consistent across both flows.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn connect_websocket(
|
||||
&self,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
@@ -638,6 +657,7 @@ impl ModelClient {
|
||||
///
|
||||
/// Callers should pass the current turn-state lock when available so sticky-routing state is
|
||||
/// replayed on reconnect within the same turn.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_websocket_headers(
|
||||
&self,
|
||||
turn_state: Option<&Arc<OnceLock<String>>>,
|
||||
@@ -678,13 +698,21 @@ impl Drop for ModelClientSession {
|
||||
|
||||
impl ModelClientSession {
|
||||
fn reset_websocket_session(&mut self) {
|
||||
self.websocket_session.connection = None;
|
||||
self.reset_websocket_connection();
|
||||
self.websocket_session.last_request = None;
|
||||
self.websocket_session.last_response_rx = None;
|
||||
self.websocket_session
|
||||
.set_connection_reused(/*connection_reused*/ false);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn reset_websocket_connection(&mut self) {
|
||||
self.websocket_session.connection = None;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn reset_websocket_connection(&mut self) {}
|
||||
|
||||
fn build_responses_request(
|
||||
&self,
|
||||
provider: &codex_api::Provider,
|
||||
@@ -817,6 +845,7 @@ impl ModelClientSession {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn get_last_response(&mut self) -> Option<LastResponse> {
|
||||
self.websocket_session
|
||||
.last_response_rx
|
||||
@@ -827,6 +856,7 @@ impl ModelClientSession {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn prepare_websocket_request(
|
||||
&mut self,
|
||||
payload: ResponseCreateWsRequest,
|
||||
@@ -858,6 +888,7 @@ impl ModelClientSession {
|
||||
/// Opportunistically preconnects a websocket for this turn-scoped client session.
|
||||
///
|
||||
/// This performs only connection setup; it never sends prompt payloads.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn preconnect_websocket(
|
||||
&mut self,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
@@ -897,7 +928,18 @@ impl ModelClientSession {
|
||||
.set_connection_reused(/*connection_reused*/ false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn preconnect_websocket(
|
||||
&mut self,
|
||||
_session_telemetry: &SessionTelemetry,
|
||||
_model_info: &ModelInfo,
|
||||
) -> std::result::Result<(), ApiError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a websocket connection for this turn.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[instrument(
|
||||
name = "model_client.websocket_connection",
|
||||
level = "info",
|
||||
@@ -1085,6 +1127,7 @@ impl ModelClientSession {
|
||||
|
||||
/// Streams a turn via the Responses API over WebSocket transport.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[instrument(
|
||||
name = "model_client.stream_responses_websocket",
|
||||
level = "info",
|
||||
@@ -1218,6 +1261,7 @@ impl ModelClientSession {
|
||||
}
|
||||
|
||||
/// Builds telemetry for the Responses API WebSocket transport.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_websocket_telemetry(
|
||||
session_telemetry: &SessionTelemetry,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
@@ -1235,6 +1279,7 @@ impl ModelClientSession {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn prewarm_websocket(
|
||||
&mut self,
|
||||
prompt: &Prompt,
|
||||
@@ -1285,6 +1330,21 @@ impl ModelClientSession {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn prewarm_websocket(
|
||||
&mut self,
|
||||
_prompt: &Prompt,
|
||||
_model_info: &ModelInfo,
|
||||
_session_telemetry: &SessionTelemetry,
|
||||
_effort: Option<ReasoningEffortConfig>,
|
||||
_summary: ReasoningSummaryConfig,
|
||||
_service_tier: Option<ServiceTier>,
|
||||
_turn_metadata_header: Option<&str>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Streams a single model request within the current turn.
|
||||
///
|
||||
@@ -1305,6 +1365,7 @@ impl ModelClientSession {
|
||||
let wire_api = self.client.state.provider.wire_api;
|
||||
match wire_api {
|
||||
WireApi::Responses => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.client.responses_websocket_enabled() {
|
||||
let request_trace = current_span_w3c_trace_context();
|
||||
match self
|
||||
@@ -1369,6 +1430,7 @@ fn parse_turn_metadata_header(turn_metadata_header: Option<&str>) -> Option<Head
|
||||
turn_metadata_header.and_then(|value| HeaderValue::from_str(value).ok())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_ws_client_metadata(turn_metadata_header: Option<&str>) -> Option<HashMap<String, String>> {
|
||||
let turn_metadata_header = parse_turn_metadata_header(turn_metadata_header)?;
|
||||
let turn_metadata = turn_metadata_header.to_str().ok()?.to_string();
|
||||
@@ -1408,6 +1470,7 @@ fn build_responses_headers(
|
||||
headers
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn map_response_stream<S>(
|
||||
api_stream: S,
|
||||
session_telemetry: SessionTelemetry,
|
||||
@@ -1421,7 +1484,7 @@ where
|
||||
let (tx_event, rx_event) = mpsc::channel::<Result<ResponseEvent>>(1600);
|
||||
let (tx_last_response, rx_last_response) = oneshot::channel::<LastResponse>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
let mut logged_error = false;
|
||||
let mut tx_last_response = Some(tx_last_response);
|
||||
let mut items_added: Vec<ResponseItem> = Vec::new();
|
||||
@@ -1490,6 +1553,87 @@ where
|
||||
(ResponseStream { rx_event }, rx_last_response)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn map_response_stream<S>(
|
||||
api_stream: S,
|
||||
session_telemetry: SessionTelemetry,
|
||||
) -> (ResponseStream, oneshot::Receiver<LastResponse>)
|
||||
where
|
||||
S: futures::Stream<Item = std::result::Result<ResponseEvent, ApiError>> + Unpin + 'static,
|
||||
{
|
||||
let (tx_event, rx_event) = mpsc::channel::<Result<ResponseEvent>>(1600);
|
||||
let (tx_last_response, rx_last_response) = oneshot::channel::<LastResponse>();
|
||||
|
||||
crate::async_runtime::spawn(async move {
|
||||
let mut logged_error = false;
|
||||
let mut tx_last_response = Some(tx_last_response);
|
||||
let mut items_added: Vec<ResponseItem> = Vec::new();
|
||||
let mut api_stream = api_stream;
|
||||
while let Some(event) = api_stream.next().await {
|
||||
match event {
|
||||
Ok(ResponseEvent::OutputItemDone(item)) => {
|
||||
items_added.push(item.clone());
|
||||
if tx_event
|
||||
.send(Ok(ResponseEvent::OutputItemDone(item)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(ResponseEvent::Completed {
|
||||
response_id,
|
||||
token_usage,
|
||||
}) => {
|
||||
if let Some(usage) = &token_usage {
|
||||
session_telemetry.sse_event_completed(
|
||||
usage.input_tokens,
|
||||
usage.output_tokens,
|
||||
Some(usage.cached_input_tokens),
|
||||
Some(usage.reasoning_output_tokens),
|
||||
usage.total_tokens,
|
||||
);
|
||||
}
|
||||
let _ = tx_event
|
||||
.send(Ok(ResponseEvent::Completed {
|
||||
response_id: response_id.clone(),
|
||||
token_usage: token_usage.clone(),
|
||||
}))
|
||||
.await;
|
||||
if let Some(tx_last_response) = tx_last_response.take() {
|
||||
let _ = tx_last_response.send(LastResponse {
|
||||
response_id,
|
||||
items_added,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
Ok(event) => {
|
||||
if tx_event.send(Ok(event)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if !logged_error {
|
||||
tracing::warn!("error while forwarding model stream event: {err:#}");
|
||||
logged_error = true;
|
||||
}
|
||||
let _ = tx_event.send(Err(map_api_error(err))).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = tx_event
|
||||
.send(Err(CodexErr::Stream(
|
||||
"model stream closed unexpectedly".into(),
|
||||
None,
|
||||
)))
|
||||
.await;
|
||||
});
|
||||
|
||||
(ResponseStream { rx_event }, rx_last_response)
|
||||
}
|
||||
|
||||
/// Handles a 401 response by optionally refreshing ChatGPT tokens once.
|
||||
///
|
||||
/// When refresh succeeds, the caller should retry the API call; otherwise
|
||||
@@ -1547,6 +1691,7 @@ impl AuthRequestTelemetryContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
struct WebsocketConnectParams<'a> {
|
||||
session_telemetry: &'a SessionTelemetry,
|
||||
api_provider: codex_api::Provider,
|
||||
@@ -1774,6 +1919,7 @@ impl SseTelemetry for ApiTelemetry {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl WebsocketTelemetry for ApiTelemetry {
|
||||
fn on_ws_request(&self, duration: Duration, error: Option<&ApiError>, connection_reused: bool) {
|
||||
let error_message = error.map(telemetry_api_error_message);
|
||||
|
||||
@@ -134,12 +134,6 @@ use futures::future::BoxFuture;
|
||||
use futures::future::Shared;
|
||||
use futures::prelude::*;
|
||||
use futures::stream::FuturesOrdered;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
use rmcp::model::PaginatedRequestParams;
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
use rmcp::model::ReadResourceResult;
|
||||
use rmcp::model::RequestId;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -270,6 +264,12 @@ use crate::mcp::with_codex_apps_mcp;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
use crate::mcp_connection_manager::filter_non_codex_apps_mcp_tools_only;
|
||||
use crate::mcp_types::ListResourceTemplatesResult;
|
||||
use crate::mcp_types::ListResourcesResult;
|
||||
use crate::mcp_types::PaginatedRequestParams;
|
||||
use crate::mcp_types::ReadResourceRequestParams;
|
||||
use crate::mcp_types::ReadResourceResult;
|
||||
use crate::mcp_types::RequestId;
|
||||
use crate::memories;
|
||||
use crate::mentions::build_connector_slug_counts;
|
||||
use crate::mentions::build_skill_name_counts;
|
||||
@@ -422,6 +422,7 @@ pub(crate) struct CodexSpawnArgs {
|
||||
pub(crate) inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
pub(crate) inherited_exec_policy: Option<Arc<ExecPolicyManager>>,
|
||||
pub(crate) user_shell_override: Option<shell::Shell>,
|
||||
pub(crate) code_mode_runtime: Option<Arc<dyn codex_code_mode::CodeModeRuntime>>,
|
||||
pub(crate) parent_trace: Option<W3cTraceContext>,
|
||||
}
|
||||
|
||||
@@ -476,6 +477,7 @@ impl Codex {
|
||||
inherited_shell_snapshot,
|
||||
user_shell_override,
|
||||
inherited_exec_policy,
|
||||
code_mode_runtime,
|
||||
parent_trace: _,
|
||||
} = args;
|
||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
@@ -518,6 +520,7 @@ impl Codex {
|
||||
warn!("{message}");
|
||||
config.startup_warnings.push(message);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if config.features.enabled(Feature::CodeMode)
|
||||
&& let Err(err) = resolve_compatible_node(config.js_repl_node_path.as_deref()).await
|
||||
{
|
||||
@@ -660,6 +663,7 @@ impl Codex {
|
||||
mcp_manager.clone(),
|
||||
skills_watcher,
|
||||
agent_control,
|
||||
code_mode_runtime,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -670,7 +674,7 @@ impl Codex {
|
||||
|
||||
// This task will run until Op::Shutdown is received.
|
||||
let session_for_loop = Arc::clone(&session);
|
||||
let session_loop_handle = tokio::spawn(async move {
|
||||
let session_loop_handle = crate::async_runtime::spawn(async move {
|
||||
submission_loop(session_for_loop, config, rx_sub)
|
||||
.instrument(info_span!("session_loop", thread_id = %thread_id))
|
||||
.await;
|
||||
@@ -1335,7 +1339,7 @@ impl Session {
|
||||
fn start_skills_watcher_listener(self: &Arc<Self>) {
|
||||
let mut rx = self.services.skills_watcher.subscribe();
|
||||
let weak_sess = Arc::downgrade(self);
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(SkillsWatcherEvent::SkillsChanged { .. }) => {
|
||||
@@ -1478,6 +1482,7 @@ impl Session {
|
||||
mcp_manager: Arc<McpManager>,
|
||||
skills_watcher: Arc<SkillsWatcher>,
|
||||
agent_control: AgentControl,
|
||||
code_mode_runtime: Option<Arc<dyn codex_code_mode::CodeModeRuntime>>,
|
||||
) -> anyhow::Result<Arc<Self>> {
|
||||
debug!(
|
||||
"Configuring session: model={}; provider={:?}",
|
||||
@@ -1903,8 +1908,9 @@ impl Session {
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
Self::build_model_client_beta_features_header(config.as_ref()),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||||
config.js_repl_node_path.clone(),
|
||||
code_mode_service: code_mode_runtime.map_or_else(
|
||||
|| crate::tools::code_mode::CodeModeService::new(config.js_repl_node_path.clone()),
|
||||
crate::tools::code_mode::CodeModeService::from_runtime,
|
||||
),
|
||||
environment: environment_manager.current().await?,
|
||||
};
|
||||
@@ -3228,14 +3234,13 @@ impl Session {
|
||||
"Overwriting existing pending elicitation for server_name: {server_name}, request_id: {request_id}"
|
||||
);
|
||||
}
|
||||
let id = match request_id {
|
||||
rmcp::model::NumberOrString::String(value) => {
|
||||
codex_protocol::mcp::RequestId::String(value.to_string())
|
||||
}
|
||||
rmcp::model::NumberOrString::Number(value) => {
|
||||
codex_protocol::mcp::RequestId::Integer(value)
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let id = match request_id.clone() {
|
||||
RequestId::String(value) => codex_protocol::mcp::RequestId::String(value.to_string()),
|
||||
RequestId::Number(value) => codex_protocol::mcp::RequestId::Integer(value),
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let id = request_id.clone();
|
||||
let event = EventMsg::ElicitationRequest(ElicitationRequestEvent {
|
||||
turn_id: params.turn_id,
|
||||
server_name,
|
||||
@@ -4645,6 +4650,8 @@ mod handlers {
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
|
||||
use crate::context_manager::is_user_turn_boundary;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::mcp_types::RequestId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
@@ -4792,7 +4799,7 @@ mod handlers {
|
||||
sess.active_turn_context_and_cancellation_token().await
|
||||
{
|
||||
let session = Arc::clone(sess);
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
execute_user_shell_command(
|
||||
session,
|
||||
turn_context,
|
||||
@@ -4837,12 +4844,13 @@ mod handlers {
|
||||
content,
|
||||
meta,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let request_id = match request_id {
|
||||
ProtocolRequestId::String(value) => {
|
||||
rmcp::model::NumberOrString::String(std::sync::Arc::from(value))
|
||||
}
|
||||
ProtocolRequestId::Integer(value) => rmcp::model::NumberOrString::Number(value),
|
||||
ProtocolRequestId::String(value) => RequestId::String(value.into()),
|
||||
ProtocolRequestId::Integer(value) => RequestId::Number(value),
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let request_id = request_id;
|
||||
if let Err(err) = sess
|
||||
.resolve_elicitation(server_name, request_id, response)
|
||||
.await
|
||||
@@ -4935,7 +4943,7 @@ mod handlers {
|
||||
pub async fn add_to_history(sess: &Arc<Session>, config: &Arc<Config>, text: String) {
|
||||
let id = sess.conversation_id;
|
||||
let config = Arc::clone(config);
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
if let Err(e) = crate::message_history::append_entry(&text, &id, &config).await {
|
||||
warn!("failed to append to message history: {e}");
|
||||
}
|
||||
@@ -4952,7 +4960,7 @@ mod handlers {
|
||||
let config = Arc::clone(config);
|
||||
let sess_clone = Arc::clone(sess);
|
||||
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
// Run lookup in blocking thread because it does file IO + locking.
|
||||
let entry_opt = tokio::task::spawn_blocking(move || {
|
||||
crate::message_history::lookup(log_id, offset, &config)
|
||||
|
||||
@@ -93,6 +93,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
inherited_shell_snapshot: None,
|
||||
user_shell_override: None,
|
||||
inherited_exec_policy: Some(Arc::clone(&parent_session.services.exec_policy)),
|
||||
code_mode_runtime: None,
|
||||
parent_trace: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -111,7 +112,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
// context when the later legacy RequestUserInput approval event only carries
|
||||
// a call_id plus approval question metadata.
|
||||
let pending_mcp_invocations = Arc::new(Mutex::new(HashMap::<String, McpInvocation>::new()));
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
forward_events(
|
||||
codex_for_events,
|
||||
tx_sub,
|
||||
@@ -125,7 +126,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
|
||||
// Forward ops from the caller to the sub-agent.
|
||||
let codex_for_ops = Arc::clone(&codex);
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
forward_ops(codex_for_ops, rx_ops, cancel_token_ops).await;
|
||||
});
|
||||
|
||||
|
||||
@@ -456,6 +456,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
inherited_shell_snapshot: None,
|
||||
inherited_exec_policy: Some(Arc::new(parent_exec_policy)),
|
||||
user_shell_override: None,
|
||||
code_mode_runtime: None,
|
||||
parent_trace: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -155,6 +155,9 @@ const RESERVED_MODEL_PROVIDER_IDS: [&str; 3] = [
|
||||
];
|
||||
|
||||
fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option<PathBuf> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let raw = std::env::var("CODEX_SQLITE_HOME").ok()?;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let raw = std::env::var(codex_state::SQLITE_HOME_ENV).ok()?;
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -634,15 +637,25 @@ impl ConfigBuilder {
|
||||
cloud_requirements,
|
||||
fallback_cwd,
|
||||
} = self;
|
||||
let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?;
|
||||
let codex_home = codex_home
|
||||
.map_or_else(find_codex_home, std::io::Result::Ok)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(error.kind(), format!("resolve codex_home failed: {error}"))
|
||||
})?;
|
||||
let cli_overrides = cli_overrides.unwrap_or_default();
|
||||
let mut harness_overrides = harness_overrides.unwrap_or_default();
|
||||
let loader_overrides = loader_overrides.unwrap_or_default();
|
||||
let cwd_override = harness_overrides.cwd.as_deref().or(fallback_cwd.as_deref());
|
||||
let cwd = match cwd_override {
|
||||
Some(path) => AbsolutePathBuf::relative_to_current_dir(path)?,
|
||||
None => AbsolutePathBuf::current_dir()?,
|
||||
};
|
||||
Some(path) if path.is_absolute() => {
|
||||
AbsolutePathBuf::try_from(normalize_for_native_workdir(path))
|
||||
}
|
||||
Some(path) => AbsolutePathBuf::relative_to_current_dir(path),
|
||||
None => AbsolutePathBuf::current_dir(),
|
||||
}
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(error.kind(), format!("resolve cwd failed: {error}"))
|
||||
})?;
|
||||
harness_overrides.cwd = Some(cwd.to_path_buf());
|
||||
let config_layer_stack = load_config_layers_state(
|
||||
&codex_home,
|
||||
@@ -651,7 +664,10 @@ impl ConfigBuilder {
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(error.kind(), format!("load config layers failed: {error}"))
|
||||
})?;
|
||||
let merged_toml = config_layer_stack.effective_config();
|
||||
|
||||
// Note that each layer in ConfigLayerStack should have resolved
|
||||
@@ -679,6 +695,12 @@ impl ConfigBuilder {
|
||||
codex_home,
|
||||
config_layer_stack,
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("assemble final config from merged layers failed: {error}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1964,6 +1986,168 @@ pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Build a session config from the default config shape plus explicit host
|
||||
/// paths, without reading config files from disk.
|
||||
pub fn load_embedded_defaults(codex_home: PathBuf, cwd: PathBuf) -> std::io::Result<Self> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
return Self::load_embedded_defaults_wasm(codex_home, cwd);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let overrides = ConfigOverrides {
|
||||
cwd: Some(cwd),
|
||||
..ConfigOverrides::default()
|
||||
};
|
||||
Self::load_config_with_layer_stack(
|
||||
ConfigToml::default(),
|
||||
overrides,
|
||||
codex_home,
|
||||
ConfigLayerStack::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn load_embedded_defaults_wasm(codex_home: PathBuf, cwd: PathBuf) -> std::io::Result<Self> {
|
||||
let cfg = ConfigToml::default();
|
||||
let model_provider_id = cfg
|
||||
.model_provider
|
||||
.clone()
|
||||
.unwrap_or_else(|| OPENAI_PROVIDER_ID.to_string());
|
||||
let model_providers = built_in_model_providers(/*openai_base_url*/ None);
|
||||
let model_provider = model_providers
|
||||
.get(&model_provider_id)
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Model provider `{model_provider_id}` not found"),
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir(cwd.as_path()))
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(error.kind(), format!("resolve session cwd failed: {error}"))
|
||||
})?;
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
|
||||
let features = ManagedFeatures::from_configured(Features::with_defaults(), None)?;
|
||||
|
||||
Ok(Self {
|
||||
config_layer_stack: ConfigLayerStack::default(),
|
||||
startup_warnings: Vec::new(),
|
||||
model: cfg.model,
|
||||
service_tier: cfg.service_tier,
|
||||
review_model: cfg.review_model,
|
||||
model_context_window: cfg.model_context_window,
|
||||
model_auto_compact_token_limit: cfg.model_auto_compact_token_limit,
|
||||
model_provider_id,
|
||||
model_provider,
|
||||
personality: cfg.personality.or(Some(Personality::Pragmatic)),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(sandbox_policy.clone()),
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
windows_sandbox_private_desktop: true,
|
||||
},
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
|
||||
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
|
||||
show_raw_agent_reasoning: cfg.show_raw_agent_reasoning.unwrap_or(false),
|
||||
user_instructions: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: cfg.developer_instructions,
|
||||
guardian_developer_instructions: None,
|
||||
compact_prompt: cfg.compact_prompt,
|
||||
commit_attribution: cfg.commit_attribution,
|
||||
notify: cfg.notify,
|
||||
tui_notifications: Notifications::default(),
|
||||
tui_notification_method: NotificationMethod::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
cwd: resolved_cwd,
|
||||
cli_auth_credentials_store_mode: cfg.cli_auth_credentials_store.unwrap_or_default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: cfg.mcp_oauth_credentials_store.unwrap_or_default(),
|
||||
mcp_oauth_callback_port: cfg.mcp_oauth_callback_port,
|
||||
mcp_oauth_callback_url: cfg.mcp_oauth_callback_url,
|
||||
model_providers,
|
||||
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
|
||||
project_doc_fallback_filenames: cfg.project_doc_fallback_filenames.unwrap_or_default(),
|
||||
tool_output_token_limit: cfg.tool_output_token_limit,
|
||||
agent_max_threads: DEFAULT_AGENT_MAX_THREADS,
|
||||
agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS,
|
||||
agent_max_depth: DEFAULT_AGENT_MAX_DEPTH,
|
||||
agent_roles: BTreeMap::new(),
|
||||
memories: cfg.memories.unwrap_or_default().into(),
|
||||
codex_home: codex_home.clone(),
|
||||
sqlite_home: codex_home.clone(),
|
||||
log_dir: codex_home.join("log"),
|
||||
history: cfg.history.unwrap_or_default(),
|
||||
ephemeral: true,
|
||||
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
main_execve_wrapper_exe: None,
|
||||
js_repl_node_path: None,
|
||||
js_repl_node_module_dirs: Vec::new(),
|
||||
zsh_path: None,
|
||||
model_reasoning_effort: cfg.model_reasoning_effort,
|
||||
plan_mode_reasoning_effort: cfg.plan_mode_reasoning_effort,
|
||||
model_reasoning_summary: cfg.model_reasoning_summary,
|
||||
model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries,
|
||||
model_catalog: None,
|
||||
model_verbosity: cfg.model_verbosity,
|
||||
chatgpt_base_url: cfg
|
||||
.chatgpt_base_url
|
||||
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_ws_base_url: cfg.experimental_realtime_ws_base_url,
|
||||
experimental_realtime_ws_model: cfg.experimental_realtime_ws_model,
|
||||
realtime: cfg
|
||||
.realtime
|
||||
.map_or_else(RealtimeConfig::default, |realtime| RealtimeConfig {
|
||||
version: realtime.version.unwrap_or_default(),
|
||||
session_type: realtime.session_type.unwrap_or_default(),
|
||||
}),
|
||||
experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt,
|
||||
experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context,
|
||||
experimental_realtime_start_instructions: cfg.experimental_realtime_start_instructions,
|
||||
forced_chatgpt_workspace_id: cfg.forced_chatgpt_workspace_id,
|
||||
forced_login_method: cfg.forced_login_method,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
web_search_config: None,
|
||||
use_experimental_unified_exec_tool: false,
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features,
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: None,
|
||||
active_project: ProjectConfig { trust_level: None },
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
notices: Notice::default(),
|
||||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
analytics_enabled: Some(true),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
otel: OtelConfig::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn load_from_base_config_with_overrides(
|
||||
cfg: ConfigToml,
|
||||
@@ -2067,7 +2251,13 @@ impl Config {
|
||||
},
|
||||
feature_overrides,
|
||||
);
|
||||
let features = ManagedFeatures::from_configured(configured_features, feature_requirements)?;
|
||||
let features = ManagedFeatures::from_configured(configured_features, feature_requirements)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("resolve managed features failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile);
|
||||
let windows_sandbox_private_desktop =
|
||||
resolve_windows_sandbox_private_desktop(&cfg, &config_profile);
|
||||
@@ -2088,11 +2278,20 @@ impl Config {
|
||||
current
|
||||
}
|
||||
}
|
||||
}))?;
|
||||
}))
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(error.kind(), format!("resolve session cwd failed: {error}"))
|
||||
})?;
|
||||
let mut additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
|
||||
.into_iter()
|
||||
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("normalize additional writable roots failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
let active_project = cfg
|
||||
.get_active_project(resolved_cwd.as_path())
|
||||
.unwrap_or(ProjectConfig { trust_level: None });
|
||||
@@ -2125,8 +2324,26 @@ impl Config {
|
||||
None => WindowsSandboxLevel::from_features(&features),
|
||||
};
|
||||
let memories_root = memory_root(&codex_home);
|
||||
std::fs::create_dir_all(&memories_root)?;
|
||||
let memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
std::fs::create_dir_all(&memories_root).map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!(
|
||||
"create memories root {} failed: {error}",
|
||||
memories_root.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let memories_root =
|
||||
AbsolutePathBuf::from_absolute_path(&memories_root).map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!(
|
||||
"normalize memories root {} failed: {error}",
|
||||
memories_root.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
if !additional_writable_roots
|
||||
.iter()
|
||||
.any(|existing| existing == &memories_root)
|
||||
@@ -2157,17 +2374,36 @@ impl Config {
|
||||
"default_permissions requires a named permissions profile",
|
||||
)
|
||||
})?;
|
||||
let profile = resolve_permission_profile(permissions, default_permissions)?;
|
||||
let profile =
|
||||
resolve_permission_profile(permissions, default_permissions).map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!(
|
||||
"resolve permissions profile `{default_permissions}` failed: {error}"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let configured_network_proxy_config =
|
||||
network_proxy_config_from_profile_network(profile.network.as_ref());
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
compile_permission_profile(
|
||||
permissions,
|
||||
default_permissions,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) = compile_permission_profile(
|
||||
permissions,
|
||||
default_permissions,
|
||||
&mut startup_warnings,
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("compile permissions profile `{default_permissions}` failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
let mut sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("project filesystem policy conversion failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_writable_roots(
|
||||
@@ -2175,7 +2411,15 @@ impl Config {
|
||||
&additional_writable_roots,
|
||||
);
|
||||
sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!(
|
||||
"project filesystem policy conversion after writable roots failed: {error}"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
@@ -2244,12 +2488,18 @@ impl Config {
|
||||
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
|
||||
|
||||
let agent_roles =
|
||||
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?;
|
||||
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(error.kind(), format!("load agent roles failed: {error}"))
|
||||
})?;
|
||||
|
||||
let openai_base_url = cfg
|
||||
.openai_base_url
|
||||
.clone()
|
||||
.filter(|value| !value.is_empty());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let openai_base_url_from_env = None;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let openai_base_url_from_env = std::env::var(OPENAI_BASE_URL_ENV_VAR)
|
||||
.ok()
|
||||
.filter(|value| !value.is_empty());
|
||||
@@ -2409,8 +2659,17 @@ impl Config {
|
||||
.model_instructions_file
|
||||
.as_ref()
|
||||
.or(cfg.model_instructions_file.as_ref());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let file_base_instructions = None;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let file_base_instructions =
|
||||
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?;
|
||||
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("load base instructions failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
let base_instructions = base_instructions.or(file_base_instructions);
|
||||
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
|
||||
let guardian_developer_instructions = guardian_developer_instructions_from_requirements(
|
||||
@@ -2429,10 +2688,16 @@ impl Config {
|
||||
.experimental_compact_prompt_file
|
||||
.as_ref()
|
||||
.or(cfg.experimental_compact_prompt_file.as_ref());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let file_compact_prompt = None;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let file_compact_prompt = Self::try_read_non_empty_file(
|
||||
experimental_compact_prompt_path,
|
||||
"experimental compact prompt file",
|
||||
)?;
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(error.kind(), format!("load compact prompt failed: {error}"))
|
||||
})?;
|
||||
let compact_prompt = compact_prompt.or(file_compact_prompt);
|
||||
let js_repl_node_path = js_repl_node_path_override
|
||||
.or(config_profile.js_repl_node_path.map(Into::into))
|
||||
@@ -2460,7 +2725,10 @@ impl Config {
|
||||
.model_catalog_json
|
||||
.clone()
|
||||
.or(cfg.model_catalog_json.clone()),
|
||||
)?;
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(error.kind(), format!("load model catalog failed: {error}"))
|
||||
})?;
|
||||
|
||||
let log_dir = cfg
|
||||
.log_dir
|
||||
@@ -2471,6 +2739,9 @@ impl Config {
|
||||
p.push("log");
|
||||
p
|
||||
});
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let sqlite_home = codex_home.to_path_buf();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let sqlite_home = cfg
|
||||
.sqlite_home
|
||||
.as_ref()
|
||||
@@ -2484,19 +2755,37 @@ impl Config {
|
||||
approval_policy,
|
||||
&mut constrained_approval_policy,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("apply approval policy constraints failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
apply_requirement_constrained_value(
|
||||
"sandbox_mode",
|
||||
sandbox_policy,
|
||||
&mut constrained_sandbox_policy,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("apply sandbox policy constraints failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
apply_requirement_constrained_value(
|
||||
"web_search_mode",
|
||||
web_search_mode,
|
||||
&mut constrained_web_search_mode,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("apply web search constraints failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mcp_servers = constrain_mcp_servers(cfg.mcp_servers.clone(), mcp_servers.as_ref())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
|
||||
@@ -2520,12 +2809,21 @@ impl Config {
|
||||
} else {
|
||||
err
|
||||
}
|
||||
})
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("build network proxy configuration failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
let network = if has_network_requirements {
|
||||
Some(network)
|
||||
} else {
|
||||
network.enabled().then_some(network)
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let helper_readable_roots = Vec::new();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let helper_readable_roots = get_readable_roots_required_for_codex_runtime(
|
||||
&codex_home,
|
||||
zsh_path.as_ref(),
|
||||
|
||||
@@ -137,7 +137,14 @@ impl NetworkProxySpec {
|
||||
None => builder.policy_decider(|_request| async {
|
||||
// In restricted sandbox modes, allowlist misses should ask for
|
||||
// explicit network approval instead of hard-denying.
|
||||
NetworkDecision::ask("not_allowed")
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
NetworkDecision::ask("not_allowed")
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Ok(NetworkDecision::ask("not_allowed"))
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -434,7 +434,7 @@ async fn create_empty_user_layer(
|
||||
} = resolve_symlink_write_paths(config_toml.as_path())
|
||||
.map_err(|err| ConfigServiceError::io("failed to resolve user config path", err))?;
|
||||
let toml_value = match read_path {
|
||||
Some(path) => match tokio::fs::read_to_string(&path).await {
|
||||
Some(path) => match crate::async_fs::read_to_string(&path).await {
|
||||
Ok(contents) => toml::from_str(&contents).map_err(|e| {
|
||||
ConfigServiceError::toml("failed to parse existing user config.toml", e)
|
||||
})?,
|
||||
|
||||
140
codex-rs/core/src/config_loader_wasm.rs
Normal file
140
codex-rs/core/src/config_loader_wasm.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use crate::config::ConfigToml;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigRequirementsWithSources;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
pub use codex_config::AppRequirementToml;
|
||||
pub use codex_config::AppsRequirementsToml;
|
||||
pub use codex_config::CloudRequirementsLoadError;
|
||||
pub use codex_config::CloudRequirementsLoadErrorCode;
|
||||
pub use codex_config::CloudRequirementsLoader;
|
||||
pub use codex_config::ConfigError;
|
||||
pub use codex_config::ConfigLayerEntry;
|
||||
pub use codex_config::ConfigLayerStack;
|
||||
pub use codex_config::ConfigLayerStackOrdering;
|
||||
pub use codex_config::ConfigLoadError;
|
||||
pub use codex_config::ConfigRequirements;
|
||||
pub use codex_config::ConfigRequirementsToml;
|
||||
pub use codex_config::ConstrainedWithSource;
|
||||
pub use codex_config::FeatureRequirementsToml;
|
||||
pub use codex_config::LoaderOverrides;
|
||||
pub use codex_config::McpServerIdentity;
|
||||
pub use codex_config::McpServerRequirement;
|
||||
pub use codex_config::NetworkConstraints;
|
||||
pub use codex_config::NetworkDomainPermissionToml;
|
||||
pub use codex_config::NetworkDomainPermissionsToml;
|
||||
pub use codex_config::NetworkRequirementsToml;
|
||||
pub use codex_config::NetworkUnixSocketPermissionToml;
|
||||
pub use codex_config::NetworkUnixSocketPermissionsToml;
|
||||
pub use codex_config::RequirementSource;
|
||||
pub use codex_config::ResidencyRequirement;
|
||||
pub use codex_config::SandboxModeRequirement;
|
||||
pub use codex_config::Sourced;
|
||||
pub use codex_config::TextPosition;
|
||||
pub use codex_config::TextRange;
|
||||
pub use codex_config::WebSearchModeRequirement;
|
||||
pub(crate) use codex_config::build_cli_overrides_layer;
|
||||
pub(crate) use codex_config::config_error_from_toml;
|
||||
pub use codex_config::default_project_root_markers;
|
||||
pub use codex_config::format_config_error;
|
||||
pub use codex_config::format_config_error_with_source;
|
||||
pub(crate) use codex_config::io_error_from_config_error;
|
||||
pub use codex_config::merge_toml_values;
|
||||
pub use codex_config::project_root_markers_from_config;
|
||||
|
||||
pub(crate) async fn first_layer_config_error(layers: &ConfigLayerStack) -> Option<ConfigError> {
|
||||
codex_config::first_layer_config_error::<ConfigToml>(layers, CONFIG_TOML_FILE).await
|
||||
}
|
||||
|
||||
pub(crate) async fn first_layer_config_error_from_entries(
|
||||
layers: &[ConfigLayerEntry],
|
||||
) -> Option<ConfigError> {
|
||||
codex_config::first_layer_config_error_from_entries::<ConfigToml>(layers, CONFIG_TOML_FILE)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn load_config_layers_state(
|
||||
_codex_home: &Path,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
cli_overrides: &[(String, TomlValue)],
|
||||
_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
) -> io::Result<ConfigLayerStack> {
|
||||
let mut requirements_toml = ConfigRequirementsWithSources::default();
|
||||
if let Some(requirements) = cloud_requirements.get().await.map_err(io::Error::other)? {
|
||||
requirements_toml.merge_unset_fields(RequirementSource::CloudRequirements, requirements);
|
||||
}
|
||||
|
||||
let mut layers = Vec::<ConfigLayerEntry>::new();
|
||||
if !cli_overrides.is_empty() {
|
||||
let cli_layer = build_cli_overrides_layer(cli_overrides);
|
||||
let base_dir = cwd
|
||||
.as_ref()
|
||||
.map(AbsolutePathBuf::as_path)
|
||||
.unwrap_or_else(|| Path::new("."));
|
||||
let _ = AbsolutePathBuf::from_absolute_path(base_dir)?;
|
||||
layers.push(ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
resolve_relative_paths_in_config_toml(cli_layer, base_dir)?,
|
||||
));
|
||||
}
|
||||
|
||||
let requirements = requirements_toml
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(io::Error::other)?;
|
||||
ConfigLayerStack::new(layers, requirements, requirements_toml.into_toml())
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_relative_paths_in_config_toml(
|
||||
value_from_config_toml: TomlValue,
|
||||
base_dir: &Path,
|
||||
) -> io::Result<TomlValue> {
|
||||
let _guard = AbsolutePathBufGuard::new(base_dir);
|
||||
let Ok(resolved) = value_from_config_toml.clone().try_into::<ConfigToml>() else {
|
||||
return Ok(value_from_config_toml);
|
||||
};
|
||||
drop(_guard);
|
||||
|
||||
let resolved_value = TomlValue::try_from(resolved).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Failed to serialize resolved config: {err}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(copy_shape_from_original(
|
||||
&value_from_config_toml,
|
||||
&resolved_value,
|
||||
))
|
||||
}
|
||||
|
||||
fn copy_shape_from_original(original: &TomlValue, resolved: &TomlValue) -> TomlValue {
|
||||
match (original, resolved) {
|
||||
(TomlValue::Table(original_table), TomlValue::Table(resolved_table)) => {
|
||||
let mut table = toml::map::Map::new();
|
||||
for (key, original_value) in original_table {
|
||||
let resolved_value = resolved_table.get(key).unwrap_or(original_value);
|
||||
table.insert(
|
||||
key.clone(),
|
||||
copy_shape_from_original(original_value, resolved_value),
|
||||
);
|
||||
}
|
||||
TomlValue::Table(table)
|
||||
}
|
||||
(TomlValue::Array(original_array), TomlValue::Array(resolved_array)) => {
|
||||
let mut items = Vec::new();
|
||||
for (index, original_value) in original_array.iter().enumerate() {
|
||||
let resolved_value = resolved_array.get(index).unwrap_or(original_value);
|
||||
items.push(copy_shape_from_original(original_value, resolved_value));
|
||||
}
|
||||
TomlValue::Array(items)
|
||||
}
|
||||
(_, resolved_value) => resolved_value.clone(),
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ use codex_connectors::DirectoryListResponse;
|
||||
use codex_login::token_data::TokenData;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tracing::warn;
|
||||
@@ -42,6 +41,7 @@ use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp::with_codex_apps_mcp;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
use crate::mcp_types::ToolAnnotations;
|
||||
use crate::plugins::AppConnectorId;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::plugins::list_tool_suggest_discoverable_plugins;
|
||||
|
||||
168
codex-rs/core/src/exec_policy_wasm.rs
Normal file
168
codex-rs/core/src/exec_policy_wasm.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::NetworkRuleProtocol;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
|
||||
pub(crate) fn child_uses_parent_exec_policy(parent_config: &Config, child_config: &Config) -> bool {
|
||||
parent_config.config_layer_stack.requirements().exec_policy
|
||||
== child_config.config_layer_stack.requirements().exec_policy
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_is_rejected_by_policy(
|
||||
approval_policy: AskForApproval,
|
||||
_prompt_is_rule: bool,
|
||||
) -> Option<&'static str> {
|
||||
match approval_policy {
|
||||
AskForApproval::Never => {
|
||||
Some("approval required by policy, but AskForApproval is set to Never")
|
||||
}
|
||||
AskForApproval::OnFailure
|
||||
| AskForApproval::OnRequest
|
||||
| AskForApproval::UnlessTrusted
|
||||
| AskForApproval::Granular(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExecPolicyError {
|
||||
#[error("exec policy is unavailable in wasm")]
|
||||
Unsupported,
|
||||
|
||||
#[error("failed to parse rules file {path}: {source}")]
|
||||
ParsePolicy {
|
||||
path: String,
|
||||
source: codex_execpolicy::Error,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExecPolicyUpdateError {
|
||||
#[error("exec policy updates are unavailable in wasm")]
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
pub(crate) struct ExecApprovalRequest<'a> {
|
||||
pub(crate) command: &'a [String],
|
||||
pub(crate) approval_policy: AskForApproval,
|
||||
pub(crate) sandbox_policy: &'a SandboxPolicy,
|
||||
pub(crate) file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
pub(crate) sandbox_permissions: SandboxPermissions,
|
||||
pub(crate) prefix_rule: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub(crate) struct ExecPolicyManager {
|
||||
policy: Arc<Policy>,
|
||||
}
|
||||
|
||||
impl ExecPolicyManager {
|
||||
pub(crate) fn new(policy: Arc<Policy>) -> Self {
|
||||
Self { policy }
|
||||
}
|
||||
|
||||
pub(crate) async fn load(_config_stack: &ConfigLayerStack) -> Result<Self, ExecPolicyError> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub(crate) fn current(&self) -> Arc<Policy> {
|
||||
Arc::clone(&self.policy)
|
||||
}
|
||||
|
||||
pub(crate) fn compiled_network_domains(&self) -> (Vec<String>, Vec<String>) {
|
||||
(Vec::new(), Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) async fn create_exec_approval_requirement_for_command(
|
||||
&self,
|
||||
_req: ExecApprovalRequest<'_>,
|
||||
) -> ExecApprovalRequirement {
|
||||
ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
proposed_execpolicy_amendment: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn amend(
|
||||
&self,
|
||||
_amendment: &ExecPolicyAmendment,
|
||||
) -> Result<(), ExecPolicyUpdateError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn requirement_for(
|
||||
&self,
|
||||
_request: &ExecApprovalRequest<'_>,
|
||||
) -> Option<ExecApprovalRequirement> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn append_amendment_and_update(
|
||||
&self,
|
||||
_codex_home: &std::path::Path,
|
||||
_amendment: &ExecPolicyAmendment,
|
||||
) -> Result<(), ExecPolicyUpdateError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn append_network_rule_and_update(
|
||||
&self,
|
||||
_codex_home: &std::path::Path,
|
||||
_host: &str,
|
||||
_protocol: NetworkRuleProtocol,
|
||||
_decision: Decision,
|
||||
_justification: Option<String>,
|
||||
) -> Result<(), ExecPolicyUpdateError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ExecPolicyManager {
|
||||
fn default() -> Self {
|
||||
Self::new(Arc::new(Policy::empty()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_execpolicy_for_warnings(
|
||||
_config_stack: &ConfigLayerStack,
|
||||
) -> Option<ExecPolicyError> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn format_exec_policy_error_with_source(error: &ExecPolicyError) -> String {
|
||||
error.to_string()
|
||||
}
|
||||
|
||||
pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy, ExecPolicyError> {
|
||||
Ok(ExecPolicyManager::load(config_stack)
|
||||
.await?
|
||||
.current()
|
||||
.as_ref()
|
||||
.clone())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_decision_for_unmatched_command(
|
||||
_decision: &Decision,
|
||||
_approval_policy: AskForApproval,
|
||||
_sandbox_policy: &SandboxPolicy,
|
||||
_file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
_sandbox_permissions: SandboxPermissions,
|
||||
_prefix_rule: Option<&[String]>,
|
||||
_command: &[String],
|
||||
_cwd: Option<&PathBuf>,
|
||||
_windows_sandbox_kind: Option<FileSystemSandboxKind>,
|
||||
) -> Option<ExecApprovalRequirement> {
|
||||
None
|
||||
}
|
||||
235
codex-rs/core/src/exec_wasm.rs
Normal file
235
codex-rs/core/src/exec_wasm.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use async_channel::Sender;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxTransformError;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result;
|
||||
use crate::protocol::Event;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
|
||||
pub const DEFAULT_EXEC_COMMAND_TIMEOUT_MS: u64 = 10_000;
|
||||
pub const IO_DRAIN_TIMEOUT_MS: u64 = 2_000;
|
||||
pub(crate) const MAX_EXEC_OUTPUT_DELTAS_PER_CALL: usize = 10_000;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecParams {
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub expiration: ExecExpiration,
|
||||
pub capture_policy: ExecCapturePolicy,
|
||||
pub env: HashMap<String, String>,
|
||||
pub network: Option<NetworkProxy>,
|
||||
pub sandbox_permissions: SandboxPermissions,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
pub justification: Option<String>,
|
||||
pub arg0: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct WindowsRestrictedTokenFilesystemOverlay {
|
||||
pub(crate) additional_deny_write_paths: Vec<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum ExecCapturePolicy {
|
||||
#[default]
|
||||
ShellTool,
|
||||
FullBuffer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ExecExpiration {
|
||||
Timeout(Duration),
|
||||
DefaultTimeout,
|
||||
Cancellation(CancellationToken),
|
||||
}
|
||||
|
||||
impl From<Option<u64>> for ExecExpiration {
|
||||
fn from(timeout_ms: Option<u64>) -> Self {
|
||||
timeout_ms.map_or(ExecExpiration::DefaultTimeout, |timeout_ms| {
|
||||
ExecExpiration::Timeout(Duration::from_millis(timeout_ms))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for ExecExpiration {
|
||||
fn from(timeout_ms: u64) -> Self {
|
||||
ExecExpiration::Timeout(Duration::from_millis(timeout_ms))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecExpiration {
|
||||
pub(crate) async fn wait(self) {
|
||||
match self {
|
||||
ExecExpiration::Timeout(duration) => tokio::time::sleep(duration).await,
|
||||
ExecExpiration::DefaultTimeout => {
|
||||
tokio::time::sleep(Duration::from_millis(DEFAULT_EXEC_COMMAND_TIMEOUT_MS)).await
|
||||
}
|
||||
ExecExpiration::Cancellation(cancel) => cancel.cancelled().await,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn timeout_ms(&self) -> Option<u64> {
|
||||
match self {
|
||||
ExecExpiration::Timeout(duration) => Some(duration.as_millis() as u64),
|
||||
ExecExpiration::DefaultTimeout => Some(DEFAULT_EXEC_COMMAND_TIMEOUT_MS),
|
||||
ExecExpiration::Cancellation(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StdoutStream {
|
||||
pub sub_id: String,
|
||||
pub call_id: String,
|
||||
pub tx_event: Sender<Event>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StreamOutput<T: Clone> {
|
||||
pub text: T,
|
||||
pub truncated_after_lines: Option<u32>,
|
||||
}
|
||||
|
||||
impl<T: Clone> StreamOutput<T> {
|
||||
pub fn new(text: T) -> Self {
|
||||
Self {
|
||||
text,
|
||||
truncated_after_lines: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamOutput<Vec<u8>> {
|
||||
pub fn from_utf8_lossy(&self) -> StreamOutput<String> {
|
||||
StreamOutput {
|
||||
text: String::from_utf8_lossy(&self.text).into_owned(),
|
||||
truncated_after_lines: self.truncated_after_lines,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExecToolCallOutput {
|
||||
pub exit_code: i32,
|
||||
pub stdout: StreamOutput<String>,
|
||||
pub stderr: StreamOutput<String>,
|
||||
pub aggregated_output: StreamOutput<String>,
|
||||
pub duration: Duration,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
impl Default for ExecToolCallOutput {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
exit_code: 0,
|
||||
stdout: StreamOutput::new(String::new()),
|
||||
stderr: StreamOutput::new(String::new()),
|
||||
aggregated_output: StreamOutput::new(String::new()),
|
||||
duration: Duration::ZERO,
|
||||
timed_out: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn process_exec_tool_call(
|
||||
_params: ExecParams,
|
||||
_sandbox_policy: &SandboxPolicy,
|
||||
_file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
_network_sandbox_policy: NetworkSandboxPolicy,
|
||||
_sandbox_cwd: &Path,
|
||||
_codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
_use_legacy_landlock: bool,
|
||||
_stdout_stream: Option<StdoutStream>,
|
||||
) -> Result<ExecToolCallOutput> {
|
||||
Err(CodexErr::UnsupportedOperation(
|
||||
"process execution is unavailable in wasm".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn build_exec_request(
|
||||
params: ExecParams,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
_sandbox_cwd: &Path,
|
||||
_codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
_use_legacy_landlock: bool,
|
||||
) -> Result<ExecRequest> {
|
||||
Ok(ExecRequest::new(
|
||||
params.command,
|
||||
params.cwd,
|
||||
params.env,
|
||||
params.network,
|
||||
params.expiration,
|
||||
params.capture_policy,
|
||||
SandboxType::None,
|
||||
params.windows_sandbox_level,
|
||||
params.windows_sandbox_private_desktop,
|
||||
sandbox_policy.clone(),
|
||||
file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy,
|
||||
params.arg0,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_exec_request(
|
||||
_exec_request: ExecRequest,
|
||||
_effective_policy: &SandboxPolicy,
|
||||
_stdout_stream: Option<StdoutStream>,
|
||||
_after_spawn: Option<Box<dyn FnOnce() + Send>>,
|
||||
) -> Result<ExecToolCallOutput> {
|
||||
Err(CodexErr::UnsupportedOperation(
|
||||
"process execution is unavailable in wasm".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn is_likely_sandbox_denied(
|
||||
_sandbox_type: SandboxType,
|
||||
_exec_output: &ExecToolCallOutput,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported_windows_restricted_token_sandbox_reason(
|
||||
_file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
_sandbox_permissions: SandboxPermissions,
|
||||
) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_windows_restricted_token_filesystem_overlay(
|
||||
_file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
_sandbox_permissions: SandboxPermissions,
|
||||
) -> std::result::Result<Option<WindowsRestrictedTokenFilesystemOverlay>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
impl From<SandboxTransformError> for CodexErr {
|
||||
fn from(err: SandboxTransformError) -> Self {
|
||||
match err {
|
||||
SandboxTransformError::MissingLinuxSandboxExecutable => {
|
||||
CodexErr::LandlockSandboxExecutableNotProvided
|
||||
}
|
||||
SandboxTransformError::SeatbeltUnavailable => {
|
||||
CodexErr::UnsupportedOperation("seatbelt sandbox is unavailable".to_string())
|
||||
}
|
||||
other => CodexErr::UnsupportedOperation(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
1
codex-rs/core/src/landlock_wasm.rs
Normal file
1
codex-rs/core/src/landlock_wasm.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Browser/wasm builds never launch a native sandbox helper.
|
||||
@@ -9,12 +9,16 @@ pub mod api_bridge;
|
||||
mod apply_patch;
|
||||
mod apps;
|
||||
mod arc_monitor;
|
||||
mod async_fs;
|
||||
mod async_runtime;
|
||||
pub use codex_login as auth;
|
||||
mod auth_env_telemetry;
|
||||
mod client;
|
||||
mod client_common;
|
||||
pub mod codex;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "realtime_context_wasm.rs")]
|
||||
mod realtime_context;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "realtime_conversation_wasm.rs")]
|
||||
mod realtime_conversation;
|
||||
pub use codex::SteerInputError;
|
||||
mod codex_thread;
|
||||
@@ -26,6 +30,7 @@ mod codex_delegate;
|
||||
mod command_canonicalization;
|
||||
mod commit_attribution;
|
||||
pub mod config;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "config_loader_wasm.rs")]
|
||||
pub mod config_loader;
|
||||
pub mod connectors;
|
||||
mod context_manager;
|
||||
@@ -33,8 +38,10 @@ mod contextual_user_message;
|
||||
pub use codex_utils_path::env;
|
||||
mod environment_context;
|
||||
pub mod error;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "exec_wasm.rs")]
|
||||
pub mod exec;
|
||||
pub mod exec_env;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "exec_policy_wasm.rs")]
|
||||
mod exec_policy;
|
||||
pub mod external_agent_config;
|
||||
pub mod file_watcher;
|
||||
@@ -44,10 +51,13 @@ mod git_info_tests;
|
||||
mod guardian;
|
||||
mod hook_runtime;
|
||||
pub mod instructions;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "landlock_wasm.rs")]
|
||||
pub mod landlock;
|
||||
pub mod mcp;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "mcp_connection_manager_wasm.rs")]
|
||||
mod mcp_connection_manager;
|
||||
mod mcp_tool_approval_templates;
|
||||
mod mcp_types;
|
||||
pub mod models_manager;
|
||||
mod network_policy_decision;
|
||||
pub mod network_proxy_loader;
|
||||
@@ -57,12 +67,16 @@ pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD;
|
||||
pub use mcp_connection_manager::SandboxState;
|
||||
pub use text_encoding::bytes_to_string_smart;
|
||||
mod mcp_tool_call;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "memories_wasm.rs")]
|
||||
mod memories;
|
||||
pub mod mention_syntax;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "message_history_wasm.rs")]
|
||||
pub mod message_history;
|
||||
mod model_provider_info;
|
||||
mod monotonic_time;
|
||||
pub mod utils;
|
||||
pub use utils::path_utils;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "personality_migration_wasm.rs")]
|
||||
pub mod personality_migration;
|
||||
pub mod plugins;
|
||||
mod provider_auth;
|
||||
@@ -144,16 +158,21 @@ mod default_client_forwarding;
|
||||
pub mod default_client {
|
||||
pub use super::default_client_forwarding::*;
|
||||
}
|
||||
#[cfg_attr(target_arch = "wasm32", path = "project_doc_wasm.rs")]
|
||||
pub mod project_doc;
|
||||
mod rollout;
|
||||
pub(crate) mod safety;
|
||||
pub mod seatbelt;
|
||||
mod session_rollout_init_error;
|
||||
pub mod shell;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "shell_snapshot_wasm.rs")]
|
||||
pub mod shell_snapshot;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "spawn_wasm.rs")]
|
||||
pub mod spawn;
|
||||
pub mod state_db_bridge;
|
||||
pub use codex_rollout::state_db;
|
||||
pub mod state_db {
|
||||
pub use crate::state_db_bridge::*;
|
||||
}
|
||||
mod thread_rollout_truncation;
|
||||
mod tools;
|
||||
pub mod turn_diff_tracker;
|
||||
@@ -201,6 +220,7 @@ pub use client_common::Prompt;
|
||||
pub use client_common::REVIEW_PROMPT;
|
||||
pub use client_common::ResponseEvent;
|
||||
pub use client_common::ResponseStream;
|
||||
pub use codex_code_mode::CodeModeRuntime;
|
||||
pub use codex_sandboxing::get_platform_sandbox;
|
||||
pub use codex_tools::parse_tool_input_schema;
|
||||
pub use compact::content_items_to_text;
|
||||
@@ -213,4 +233,5 @@ pub use file_watcher::FileWatcherEvent;
|
||||
pub use turn_metadata::build_turn_metadata_header;
|
||||
pub mod compact;
|
||||
pub mod memory_trace;
|
||||
#[cfg_attr(target_arch = "wasm32", path = "otel_init_wasm.rs")]
|
||||
pub mod otel_init;
|
||||
|
||||
214
codex-rs/core/src/mcp_connection_manager_wasm.rs
Normal file
214
codex-rs/core/src/mcp_connection_manager_wasm.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_config::Constrained;
|
||||
use codex_protocol::approvals::ElicitationRequest;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::McpStartupFailure;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_rmcp_client::OAuthCredentialsStoreMode;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::config::types::McpServerConfig;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
use crate::mcp::auth::McpAuthStatusEntry;
|
||||
use crate::mcp_types::ListResourceTemplatesResult;
|
||||
use crate::mcp_types::ListResourcesResult;
|
||||
use crate::mcp_types::PaginatedRequestParams;
|
||||
use crate::mcp_types::ReadResourceRequestParams;
|
||||
use crate::mcp_types::ReadResourceResult;
|
||||
use crate::mcp_types::RequestId;
|
||||
use crate::mcp_types::Resource;
|
||||
use crate::mcp_types::ResourceTemplate;
|
||||
use crate::mcp_types::Tool;
|
||||
|
||||
pub const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const MCP_SANDBOX_STATE_CAPABILITY: &str = "codex/sandbox-state";
|
||||
pub const MCP_SANDBOX_STATE_METHOD: &str = "codex/sandbox-state/update";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct CodexAppsToolsCacheKey {
|
||||
account_id: Option<String>,
|
||||
chatgpt_user_id: Option<String>,
|
||||
is_workspace_account: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn codex_apps_tools_cache_key(
|
||||
auth: Option<&crate::CodexAuth>,
|
||||
) -> CodexAppsToolsCacheKey {
|
||||
let token_data = auth.and_then(|auth| auth.get_token_data().ok());
|
||||
let account_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.account_id.clone());
|
||||
let chatgpt_user_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.id_token.chatgpt_user_id.clone());
|
||||
let is_workspace_account = token_data
|
||||
.as_ref()
|
||||
.is_some_and(|token_data| token_data.id_token.is_workspace_account());
|
||||
|
||||
CodexAppsToolsCacheKey {
|
||||
account_id,
|
||||
chatgpt_user_id,
|
||||
is_workspace_account,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ToolInfo {
|
||||
pub(crate) server_name: String,
|
||||
pub(crate) tool_name: String,
|
||||
pub(crate) tool_namespace: String,
|
||||
pub(crate) tool: Tool,
|
||||
pub(crate) connector_id: Option<String>,
|
||||
pub(crate) connector_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) plugin_display_names: Vec<String>,
|
||||
pub(crate) connector_description: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn filter_non_codex_apps_mcp_tools_only(
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
) -> HashMap<String, ToolInfo> {
|
||||
mcp_tools.clone()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SandboxState {
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub sandbox_cwd: PathBuf,
|
||||
#[serde(default)]
|
||||
pub use_legacy_landlock: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct McpConnectionManager;
|
||||
|
||||
impl McpConnectionManager {
|
||||
pub(crate) fn new_uninitialized(_approval_policy: &Constrained<AskForApproval>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub(crate) fn has_servers(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn server_origin(&self, _server_name: &str) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_approval_policy(&self, _approval_policy: &Constrained<AskForApproval>) {}
|
||||
|
||||
#[allow(clippy::new_ret_no_self, clippy::too_many_arguments)]
|
||||
pub async fn new(
|
||||
_mcp_servers: &HashMap<String, McpServerConfig>,
|
||||
_store_mode: OAuthCredentialsStoreMode,
|
||||
_auth_entries: HashMap<String, McpAuthStatusEntry>,
|
||||
_approval_policy: &Constrained<AskForApproval>,
|
||||
_tx_event: Sender<Event>,
|
||||
_initial_sandbox_state: SandboxState,
|
||||
_codex_home: PathBuf,
|
||||
_codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
|
||||
_tool_plugin_provenance: ToolPluginProvenance,
|
||||
) -> (Self, CancellationToken) {
|
||||
(Self, CancellationToken::new())
|
||||
}
|
||||
|
||||
pub async fn resolve_elicitation(
|
||||
&self,
|
||||
_server_name: String,
|
||||
_id: RequestId,
|
||||
_response: ElicitationResponse,
|
||||
) -> Result<()> {
|
||||
Err(anyhow!("MCP elicitations are unavailable on wasm32"))
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_server_ready(
|
||||
&self,
|
||||
_server_name: &str,
|
||||
_timeout: Duration,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) async fn required_startup_failures(
|
||||
&self,
|
||||
required_servers: &[String],
|
||||
) -> Vec<McpStartupFailure> {
|
||||
required_servers
|
||||
.iter()
|
||||
.map(|server_name| McpStartupFailure {
|
||||
server: server_name.clone(),
|
||||
error: "MCP is unavailable on wasm32".to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn list_all_tools(&self) -> HashMap<String, ToolInfo> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
pub async fn hard_refresh_codex_apps_tools_cache(&self) -> Result<HashMap<String, ToolInfo>> {
|
||||
Ok(HashMap::new())
|
||||
}
|
||||
|
||||
pub async fn list_all_resources(&self) -> HashMap<String, Vec<Resource>> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
pub async fn list_all_resource_templates(&self) -> HashMap<String, Vec<ResourceTemplate>> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
pub async fn call_tool(
|
||||
&self,
|
||||
_server: &str,
|
||||
_tool: &str,
|
||||
_arguments: Option<serde_json::Value>,
|
||||
_meta: Option<serde_json::Value>,
|
||||
) -> Result<CallToolResult> {
|
||||
Err(anyhow!("MCP tool calls are unavailable on wasm32"))
|
||||
}
|
||||
|
||||
pub async fn list_resources(
|
||||
&self,
|
||||
_server: &str,
|
||||
_params: Option<PaginatedRequestParams>,
|
||||
) -> Result<ListResourcesResult> {
|
||||
Err(anyhow!("MCP resources are unavailable on wasm32"))
|
||||
}
|
||||
|
||||
pub async fn list_resource_templates(
|
||||
&self,
|
||||
_server: &str,
|
||||
_params: Option<PaginatedRequestParams>,
|
||||
) -> Result<ListResourceTemplatesResult> {
|
||||
Err(anyhow!("MCP resources are unavailable on wasm32"))
|
||||
}
|
||||
|
||||
pub async fn read_resource(
|
||||
&self,
|
||||
_server: &str,
|
||||
_params: ReadResourceRequestParams,
|
||||
) -> Result<ReadResourceResult> {
|
||||
Err(anyhow!("MCP resources are unavailable on wasm32"))
|
||||
}
|
||||
|
||||
pub async fn parse_tool_name(&self, _tool_name: &str) -> Option<(String, String)> {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn notify_sandbox_state_change(&self, _sandbox_state: &SandboxState) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;
|
||||
use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template;
|
||||
use crate::mcp_types::RequestId;
|
||||
use crate::mcp_types::ToolAnnotations;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::McpInvocation;
|
||||
use crate::protocol::McpToolCallBeginEvent;
|
||||
@@ -51,7 +53,6 @@ use codex_protocol::request_user_input::RequestUserInputQuestionOption;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_rmcp_client::ElicitationAction;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
@@ -792,9 +793,8 @@ async fn maybe_request_mcp_tool_approval(
|
||||
question.question =
|
||||
mcp_tool_approval_question_text(question.question, monitor_reason.as_deref());
|
||||
if tool_call_mcp_elicitation_enabled {
|
||||
let request_id = rmcp::model::RequestId::String(
|
||||
format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}").into(),
|
||||
);
|
||||
let request_id =
|
||||
RequestId::String(format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}").into());
|
||||
let params = build_mcp_tool_approval_elicitation_request(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
@@ -1015,7 +1015,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
connector_name: tool_info.connector_name,
|
||||
connector_description,
|
||||
tool_title: tool_info.tool.title,
|
||||
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),
|
||||
tool_description: tool_info.tool.description.map(Into::into),
|
||||
codex_apps_meta: tool_info
|
||||
.tool
|
||||
.meta
|
||||
|
||||
154
codex-rs/core/src/mcp_types.rs
Normal file
154
codex-rs/core/src/mcp_types.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::ListResourceTemplatesResult;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::ListResourcesResult;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::PaginatedRequestParams;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::ReadResourceRequestParams;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::ReadResourceResult;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::RequestId;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::Resource;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::ResourceTemplate;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::Tool;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use rmcp::model::ToolAnnotations;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) use codex_protocol::mcp::RequestId;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use schemars::JsonSchema;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use serde::Deserialize;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ToolAnnotations {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub destructive_hint: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub open_world_hint: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub read_only_hint: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Tool {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub input_schema: serde_json::Value,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub output_schema: Option<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<ToolAnnotations>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<serde_json::Value>>,
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Resource {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub uri: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<serde_json::Value>>,
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ResourceTemplate {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub uri_template: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub(crate) struct PaginatedRequestParams {
|
||||
#[serde(default)]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ListResourcesResult {
|
||||
#[serde(default)]
|
||||
pub resources: Vec<Resource>,
|
||||
#[serde(default)]
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ListResourceTemplatesResult {
|
||||
#[serde(default)]
|
||||
pub resource_templates: Vec<ResourceTemplate>,
|
||||
#[serde(default)]
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ReadResourceRequestParams {
|
||||
#[serde(default)]
|
||||
pub uri: String,
|
||||
#[serde(default)]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ReadResourceResult {
|
||||
#[serde(default)]
|
||||
pub contents: Vec<serde_json::Value>,
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
52
codex-rs/core/src/memories_wasm.rs
Normal file
52
codex-rs/core/src/memories_wasm.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::memory_citation::MemoryCitation;
|
||||
|
||||
use crate::codex::Session;
|
||||
use crate::config::Config;
|
||||
|
||||
pub(crate) mod citations {
|
||||
use super::*;
|
||||
|
||||
pub fn parse_memory_citation(_citations: Vec<String>) -> Option<MemoryCitation> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_thread_id_from_citations(_citations: Vec<String>) -> Vec<ThreadId> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod prompts {
|
||||
use super::*;
|
||||
|
||||
pub(crate) async fn build_memory_tool_developer_instructions(
|
||||
_codex_home: &Path,
|
||||
) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod usage {
|
||||
use crate::tools::context::ToolInvocation;
|
||||
|
||||
pub(crate) async fn emit_metric_for_tool_read(_invocation: &ToolInvocation, _success: bool) {}
|
||||
}
|
||||
|
||||
pub(crate) fn start_memories_startup_task(
|
||||
_sess: &Arc<Session>,
|
||||
_config: Arc<Config>,
|
||||
_session_source: &codex_protocol::protocol::SessionSource,
|
||||
) {
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_memory_root_contents(memory_root: &Path) -> std::io::Result<()> {
|
||||
crate::async_fs::create_dir_all(memory_root).await
|
||||
}
|
||||
|
||||
pub fn memory_root(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join("memories")
|
||||
}
|
||||
@@ -93,7 +93,7 @@ async fn prepare_trace(index: usize, path: &Path) -> Result<PreparedTrace> {
|
||||
}
|
||||
|
||||
async fn load_trace_text(path: &Path) -> Result<String> {
|
||||
let raw = tokio::fs::read(path).await?;
|
||||
let raw = crate::async_fs::read(path).await?;
|
||||
Ok(decode_trace_bytes(&raw))
|
||||
}
|
||||
|
||||
|
||||
24
codex-rs/core/src/message_history_wasm.rs
Normal file
24
codex-rs/core/src/message_history_wasm.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use crate::config::Config;
|
||||
use codex_protocol::ThreadId;
|
||||
|
||||
pub struct HistoryEntry {
|
||||
pub session_id: String,
|
||||
pub ts: u64,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub async fn append_entry(
|
||||
_text: &str,
|
||||
_conversation_id: &ThreadId,
|
||||
_config: &Config,
|
||||
) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn history_metadata(_config: &Config) -> (u64, usize) {
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
pub fn lookup(_log_id: u64, _offset: usize, _config: &Config) -> Option<HistoryEntry> {
|
||||
None
|
||||
}
|
||||
@@ -7,7 +7,6 @@ use std::io;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
|
||||
@@ -102,7 +101,7 @@ impl ModelsCacheManager {
|
||||
}
|
||||
|
||||
async fn load(&self) -> io::Result<Option<ModelsCache>> {
|
||||
match fs::read(&self.cache_path).await {
|
||||
match crate::async_fs::read(&self.cache_path).await {
|
||||
Ok(contents) => {
|
||||
let cache = serde_json::from_slice(&contents)
|
||||
.map_err(|err| io::Error::new(ErrorKind::InvalidData, err.to_string()))?;
|
||||
@@ -115,11 +114,11 @@ impl ModelsCacheManager {
|
||||
|
||||
async fn save_internal(&self, cache: &ModelsCache) -> io::Result<()> {
|
||||
if let Some(parent) = self.cache_path.parent() {
|
||||
fs::create_dir_all(parent).await?;
|
||||
crate::async_fs::create_dir_all(parent).await?;
|
||||
}
|
||||
let json = serde_json::to_vec_pretty(cache)
|
||||
.map_err(|err| io::Error::new(ErrorKind::InvalidData, err.to_string()))?;
|
||||
fs::write(&self.cache_path, json).await
|
||||
crate::async_fs::write(&self.cache_path, json).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
40
codex-rs/core/src/monotonic_time.rs
Normal file
40
codex-rs/core/src/monotonic_time.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use std::time::Instant;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
|
||||
pub(crate) struct Instant {
|
||||
millis_since_epoch: f64,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl Instant {
|
||||
pub(crate) fn now() -> Self {
|
||||
Self {
|
||||
millis_since_epoch: js_sys::Date::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn elapsed(self) -> Duration {
|
||||
Self::now().saturating_duration_since(self)
|
||||
}
|
||||
|
||||
pub(crate) fn duration_since(self, earlier: Self) -> Duration {
|
||||
self.saturating_duration_since(earlier)
|
||||
}
|
||||
|
||||
pub(crate) fn saturating_duration_since(self, earlier: Self) -> Duration {
|
||||
duration_from_millis((self.millis_since_epoch - earlier.millis_since_epoch).max(0.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn duration_from_millis(millis: f64) -> Duration {
|
||||
if !millis.is_finite() || millis <= 0.0 {
|
||||
return Duration::ZERO;
|
||||
}
|
||||
|
||||
Duration::from_secs_f64(millis / 1_000.0)
|
||||
}
|
||||
16
codex-rs/core/src/otel_init_wasm.rs
Normal file
16
codex-rs/core/src/otel_init_wasm.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use crate::config::Config;
|
||||
use codex_otel::OtelProvider;
|
||||
use std::error::Error;
|
||||
|
||||
pub fn build_provider(
|
||||
_config: &Config,
|
||||
_service_version: &str,
|
||||
_service_name_override: Option<&str>,
|
||||
_default_analytics_enabled: bool,
|
||||
) -> Result<Option<OtelProvider>, Box<dyn Error>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn codex_export_filter(_meta: &tracing::Metadata<'_>) -> bool {
|
||||
false
|
||||
}
|
||||
19
codex-rs/core/src/personality_migration_wasm.rs
Normal file
19
codex-rs/core/src/personality_migration_wasm.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PersonalityMigrationStatus {
|
||||
Applied,
|
||||
SkippedMarker,
|
||||
SkippedExplicitPersonality,
|
||||
SkippedNoSessions,
|
||||
}
|
||||
|
||||
pub async fn maybe_migrate_personality(
|
||||
_codex_home: &Path,
|
||||
_config_toml: &TomlValue,
|
||||
) -> io::Result<PersonalityMigrationStatus> {
|
||||
Ok(PersonalityMigrationStatus::SkippedNoSessions)
|
||||
}
|
||||
@@ -1019,7 +1019,7 @@ impl PluginsManager {
|
||||
|
||||
let config = config.clone();
|
||||
let manager = Arc::clone(self);
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
let auth = auth_manager.auth().await;
|
||||
if let Err(err) = manager
|
||||
.featured_plugin_ids_for_config(&config, auth.as_ref())
|
||||
|
||||
@@ -163,7 +163,7 @@ pub(super) fn start_startup_remote_plugin_sync_once(
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
if marker_path.is_file() {
|
||||
return;
|
||||
}
|
||||
@@ -236,9 +236,9 @@ async fn wait_for_startup_remote_plugin_sync_prerequisites(codex_home: &Path) ->
|
||||
async fn write_startup_remote_plugin_sync_marker(codex_home: &Path) -> std::io::Result<()> {
|
||||
let marker_path = startup_remote_plugin_sync_marker_path(codex_home);
|
||||
if let Some(parent) = marker_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
crate::async_fs::create_dir_all(parent).await?;
|
||||
}
|
||||
tokio::fs::write(marker_path, b"ok\n").await
|
||||
crate::async_fs::write(marker_path, b"ok\n").await
|
||||
}
|
||||
|
||||
fn prepare_curated_repo_parent_and_temp_dir(repo_path: &Path) -> Result<TempDir, String> {
|
||||
|
||||
20
codex-rs/core/src/project_doc_wasm.rs
Normal file
20
codex-rs/core/src/project_doc_wasm.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
pub(crate) const HIERARCHICAL_AGENTS_MESSAGE: &str = "";
|
||||
pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md";
|
||||
pub const LOCAL_PROJECT_DOC_FILENAME: &str = "AGENTS.override.md";
|
||||
|
||||
pub(crate) async fn get_user_instructions(_config: &Config) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn read_project_docs(_config: &Config) -> io::Result<Option<String>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn discover_project_doc_paths(_config: &Config) -> io::Result<Vec<PathBuf>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
8
codex-rs/core/src/realtime_context_wasm.rs
Normal file
8
codex-rs/core/src/realtime_context_wasm.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::codex::Session;
|
||||
|
||||
pub(crate) async fn build_realtime_startup_context(
|
||||
_sess: &Session,
|
||||
_budget_tokens: usize,
|
||||
) -> Option<String> {
|
||||
None
|
||||
}
|
||||
67
codex-rs/core/src/realtime_conversation_wasm.rs
Normal file
67
codex-rs/core/src/realtime_conversation_wasm.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::codex::Session;
|
||||
use crate::error::Result as CodexResult;
|
||||
use codex_protocol::protocol::ConversationAudioParams;
|
||||
use codex_protocol::protocol::ConversationStartParams;
|
||||
use codex_protocol::protocol::ConversationTextParams;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RealtimeConversationManager {
|
||||
active_handoff: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
impl RealtimeConversationManager {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) async fn running_state(&self) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn handoff_out(&self, _output_text: String) -> CodexResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handoff_complete(&self) -> CodexResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn active_handoff_id(&self) -> Option<String> {
|
||||
self.active_handoff.lock().await.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_active_handoff(&self) {
|
||||
*self.active_handoff.lock().await = None;
|
||||
}
|
||||
|
||||
pub(crate) async fn shutdown(&self) -> CodexResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_start(
|
||||
_sess: &Arc<Session>,
|
||||
_sub_id: String,
|
||||
_params: ConversationStartParams,
|
||||
) -> CodexResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_audio(
|
||||
_sess: &Arc<Session>,
|
||||
_sub_id: String,
|
||||
_params: ConversationAudioParams,
|
||||
) {
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_text(
|
||||
_sess: &Arc<Session>,
|
||||
_sub_id: String,
|
||||
_params: ConversationTextParams,
|
||||
) {
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_close(_sess: &Arc<Session>, _sub_id: String) {}
|
||||
@@ -1,63 +1,494 @@
|
||||
use crate::config::Config;
|
||||
pub use codex_rollout::ARCHIVED_SESSIONS_SUBDIR;
|
||||
pub use codex_rollout::INTERACTIVE_SESSION_SOURCES;
|
||||
pub use codex_rollout::RolloutRecorder;
|
||||
pub use codex_rollout::RolloutRecorderParams;
|
||||
pub use codex_rollout::SESSIONS_SUBDIR;
|
||||
pub use codex_rollout::SessionMeta;
|
||||
pub use codex_rollout::append_thread_name;
|
||||
pub use codex_rollout::find_archived_thread_path_by_id_str;
|
||||
#[deprecated(note = "use find_thread_path_by_id_str")]
|
||||
pub use codex_rollout::find_conversation_path_by_id_str;
|
||||
pub use codex_rollout::find_thread_name_by_id;
|
||||
pub use codex_rollout::find_thread_path_by_id_str;
|
||||
pub use codex_rollout::find_thread_path_by_name_str;
|
||||
pub use codex_rollout::rollout_date_parts;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native {
|
||||
use crate::config::Config;
|
||||
|
||||
impl codex_rollout::RolloutConfigView for Config {
|
||||
fn codex_home(&self) -> &std::path::Path {
|
||||
self.codex_home.as_path()
|
||||
pub use codex_rollout::ARCHIVED_SESSIONS_SUBDIR;
|
||||
pub use codex_rollout::INTERACTIVE_SESSION_SOURCES;
|
||||
pub use codex_rollout::RolloutRecorder;
|
||||
pub use codex_rollout::RolloutRecorderParams;
|
||||
pub use codex_rollout::SESSIONS_SUBDIR;
|
||||
pub use codex_rollout::SessionMeta;
|
||||
pub use codex_rollout::append_thread_name;
|
||||
pub use codex_rollout::find_archived_thread_path_by_id_str;
|
||||
#[deprecated(note = "use find_thread_path_by_id_str")]
|
||||
pub use codex_rollout::find_conversation_path_by_id_str;
|
||||
pub use codex_rollout::find_thread_name_by_id;
|
||||
pub use codex_rollout::find_thread_path_by_id_str;
|
||||
pub use codex_rollout::find_thread_path_by_name_str;
|
||||
pub use codex_rollout::rollout_date_parts;
|
||||
|
||||
impl codex_rollout::RolloutConfigView for Config {
|
||||
fn codex_home(&self) -> &std::path::Path {
|
||||
self.codex_home.as_path()
|
||||
}
|
||||
|
||||
fn sqlite_home(&self) -> &std::path::Path {
|
||||
self.sqlite_home.as_path()
|
||||
}
|
||||
|
||||
fn cwd(&self) -> &std::path::Path {
|
||||
self.cwd.as_path()
|
||||
}
|
||||
|
||||
fn model_provider_id(&self) -> &str {
|
||||
self.model_provider_id.as_str()
|
||||
}
|
||||
|
||||
fn generate_memories(&self) -> bool {
|
||||
self.memories.generate_memories
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlite_home(&self) -> &std::path::Path {
|
||||
self.sqlite_home.as_path()
|
||||
pub mod list {
|
||||
pub use codex_rollout::list::*;
|
||||
}
|
||||
|
||||
fn cwd(&self) -> &std::path::Path {
|
||||
self.cwd.as_path()
|
||||
pub(crate) mod metadata {
|
||||
pub(crate) use codex_rollout::metadata::builder_from_items;
|
||||
}
|
||||
|
||||
fn model_provider_id(&self) -> &str {
|
||||
self.model_provider_id.as_str()
|
||||
pub mod policy {
|
||||
pub use codex_rollout::policy::*;
|
||||
}
|
||||
|
||||
fn generate_memories(&self) -> bool {
|
||||
self.memories.generate_memories
|
||||
pub mod recorder {
|
||||
pub use codex_rollout::recorder::*;
|
||||
}
|
||||
|
||||
pub mod session_index {
|
||||
pub use codex_rollout::session_index::*;
|
||||
}
|
||||
|
||||
pub(crate) use crate::session_rollout_init_error::map_session_init_error;
|
||||
|
||||
pub(crate) mod truncation {
|
||||
pub(crate) use crate::thread_rollout_truncation::*;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod list {
|
||||
pub use codex_rollout::list::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm {
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::state_db::StateDbHandle;
|
||||
|
||||
pub const SESSIONS_SUBDIR: &str = "sessions";
|
||||
pub const ARCHIVED_SESSIONS_SUBDIR: &str = "archived_sessions";
|
||||
pub static INTERACTIVE_SESSION_SOURCES: LazyLock<Vec<SessionSource>> = LazyLock::new(|| {
|
||||
vec![
|
||||
SessionSource::Cli,
|
||||
SessionSource::VSCode,
|
||||
SessionSource::Custom("atlas".to_string()),
|
||||
SessionSource::Custom("chatgpt".to_string()),
|
||||
]
|
||||
});
|
||||
|
||||
pub use codex_protocol::protocol::SessionMeta;
|
||||
pub use session_index::append_thread_name;
|
||||
pub use session_index::find_thread_name_by_id;
|
||||
pub use session_index::find_thread_path_by_name_str;
|
||||
|
||||
#[deprecated(note = "use find_thread_path_by_id_str")]
|
||||
pub async fn find_conversation_path_by_id_str(
|
||||
codex_home: &Path,
|
||||
id_str: &str,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
find_thread_path_by_id_str(codex_home, id_str).await
|
||||
}
|
||||
|
||||
pub async fn find_thread_path_by_id_str(
|
||||
_codex_home: &Path,
|
||||
_id_str: &str,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn find_archived_thread_path_by_id_str(
|
||||
_codex_home: &Path,
|
||||
_id_str: &str,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn rollout_date_parts(_file_name: &OsStr) -> Option<(String, String, String)> {
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum EventPersistenceMode {
|
||||
#[default]
|
||||
Limited,
|
||||
Extended,
|
||||
}
|
||||
|
||||
pub mod policy {
|
||||
pub use super::EventPersistenceMode;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
|
||||
pub fn should_persist_response_item_for_memories(_item: &ResponseItem) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RolloutRecorder {
|
||||
rollout_path: PathBuf,
|
||||
state_db: Option<StateDbHandle>,
|
||||
_event_persistence_mode: EventPersistenceMode,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RolloutRecorderParams {
|
||||
Create {
|
||||
conversation_id: ThreadId,
|
||||
forked_from_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
base_instructions: BaseInstructions,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
event_persistence_mode: EventPersistenceMode,
|
||||
},
|
||||
Resume {
|
||||
path: PathBuf,
|
||||
event_persistence_mode: EventPersistenceMode,
|
||||
},
|
||||
}
|
||||
|
||||
impl RolloutRecorderParams {
|
||||
pub fn new(
|
||||
conversation_id: ThreadId,
|
||||
forked_from_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
base_instructions: BaseInstructions,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
event_persistence_mode: EventPersistenceMode,
|
||||
) -> Self {
|
||||
Self::Create {
|
||||
conversation_id,
|
||||
forked_from_id,
|
||||
source,
|
||||
base_instructions,
|
||||
dynamic_tools,
|
||||
event_persistence_mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resume(path: PathBuf, event_persistence_mode: EventPersistenceMode) -> Self {
|
||||
Self::Resume {
|
||||
path,
|
||||
event_persistence_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RolloutRecorder {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn list_threads(
|
||||
_config: &Config,
|
||||
_page_size: usize,
|
||||
_cursor: Option<&list::Cursor>,
|
||||
_sort_key: list::ThreadSortKey,
|
||||
_allowed_sources: &[SessionSource],
|
||||
_model_providers: Option<&[String]>,
|
||||
_default_provider: &str,
|
||||
_search_term: Option<&str>,
|
||||
) -> io::Result<list::ThreadsPage> {
|
||||
Ok(list::ThreadsPage::default())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn list_archived_threads(
|
||||
_config: &Config,
|
||||
_page_size: usize,
|
||||
_cursor: Option<&list::Cursor>,
|
||||
_sort_key: list::ThreadSortKey,
|
||||
_allowed_sources: &[SessionSource],
|
||||
_model_providers: Option<&[String]>,
|
||||
_default_provider: &str,
|
||||
_search_term: Option<&str>,
|
||||
) -> io::Result<list::ThreadsPage> {
|
||||
Ok(list::ThreadsPage::default())
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
config: &Config,
|
||||
params: RolloutRecorderParams,
|
||||
state_db: Option<StateDbHandle>,
|
||||
_state_builder: Option<metadata::ThreadMetadataBuilder>,
|
||||
) -> io::Result<Self> {
|
||||
let rollout_path = match params {
|
||||
RolloutRecorderParams::Create {
|
||||
conversation_id, ..
|
||||
} => config
|
||||
.codex_home
|
||||
.join(SESSIONS_SUBDIR)
|
||||
.join(format!("browser-{}.jsonl", conversation_id)),
|
||||
RolloutRecorderParams::Resume { path, .. } => path,
|
||||
};
|
||||
Ok(Self {
|
||||
rollout_path,
|
||||
state_db,
|
||||
_event_persistence_mode: EventPersistenceMode::Limited,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rollout_path(&self) -> &Path {
|
||||
self.rollout_path.as_path()
|
||||
}
|
||||
|
||||
pub fn state_db(&self) -> Option<StateDbHandle> {
|
||||
self.state_db.clone()
|
||||
}
|
||||
|
||||
pub async fn record_items(&self, _items: &[RolloutItem]) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn persist(&self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn flush(&self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_rollout_items(
|
||||
_path: &Path,
|
||||
) -> io::Result<(Vec<RolloutItem>, Option<ThreadId>, usize)> {
|
||||
Err(io::Error::other(
|
||||
"rollout loading is unavailable on wasm32 without a browser persistence backend",
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_rollout_history(_path: &Path) -> io::Result<InitialHistory> {
|
||||
Err(io::Error::other(
|
||||
"rollout history is unavailable on wasm32 without a browser persistence backend",
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod recorder {
|
||||
pub use super::RolloutRecorder;
|
||||
pub use super::RolloutRecorderParams;
|
||||
}
|
||||
|
||||
pub mod metadata {
|
||||
use std::path::Path;
|
||||
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ThreadMetadataBuilder;
|
||||
|
||||
pub fn builder_from_items(
|
||||
_items: &[RolloutItem],
|
||||
_rollout_path: &Path,
|
||||
) -> Option<ThreadMetadataBuilder> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub mod list {
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct ThreadsPage {
|
||||
pub items: Vec<ThreadItem>,
|
||||
pub next_cursor: Option<Cursor>,
|
||||
pub num_scanned_files: usize,
|
||||
pub reached_scan_cap: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub struct ThreadItem {
|
||||
pub path: PathBuf,
|
||||
pub thread_id: Option<ThreadId>,
|
||||
pub first_user_message: Option<String>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub git_branch: Option<String>,
|
||||
pub git_sha: Option<String>,
|
||||
pub git_origin_url: Option<String>,
|
||||
pub source: Option<SessionSource>,
|
||||
pub agent_nickname: Option<String>,
|
||||
pub agent_role: Option<String>,
|
||||
pub model_provider: Option<String>,
|
||||
pub cli_version: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThreadSortKey {
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThreadListLayout {
|
||||
NestedByDate,
|
||||
Flat,
|
||||
}
|
||||
|
||||
pub struct ThreadListConfig<'a> {
|
||||
pub allowed_sources: &'a [SessionSource],
|
||||
pub model_providers: Option<&'a [String]>,
|
||||
pub default_provider: &'a str,
|
||||
pub layout: ThreadListLayout,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cursor(String);
|
||||
|
||||
pub fn parse_cursor(cursor: &str) -> Option<Cursor> {
|
||||
if cursor.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Cursor(cursor.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_threads(
|
||||
_codex_home: &Path,
|
||||
_page_size: usize,
|
||||
_cursor: Option<&Cursor>,
|
||||
_sort_key: ThreadSortKey,
|
||||
_allowed_sources: &[SessionSource],
|
||||
_model_providers: Option<&[String]>,
|
||||
_default_provider: &str,
|
||||
) -> io::Result<ThreadsPage> {
|
||||
Ok(ThreadsPage::default())
|
||||
}
|
||||
|
||||
pub async fn get_threads_in_root(
|
||||
_root: PathBuf,
|
||||
_page_size: usize,
|
||||
_cursor: Option<&Cursor>,
|
||||
_sort_key: ThreadSortKey,
|
||||
_config: ThreadListConfig<'_>,
|
||||
) -> io::Result<ThreadsPage> {
|
||||
Ok(ThreadsPage::default())
|
||||
}
|
||||
|
||||
pub async fn read_head_for_summary(_path: &Path) -> io::Result<Vec<serde_json::Value>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub async fn read_session_meta_line(_path: &Path) -> io::Result<SessionMetaLine> {
|
||||
Ok(SessionMetaLine {
|
||||
meta: Default::default(),
|
||||
git: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn find_thread_path_by_id_str(
|
||||
_codex_home: &Path,
|
||||
_id_str: &str,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn find_archived_thread_path_by_id_str(
|
||||
_codex_home: &Path,
|
||||
_id_str: &str,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn rollout_date_parts(
|
||||
_file_name: &std::ffi::OsStr,
|
||||
) -> Option<(String, String, String)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub mod session_index {
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
|
||||
pub async fn append_thread_name(
|
||||
_codex_home: &Path,
|
||||
_thread_id: ThreadId,
|
||||
_name: &str,
|
||||
) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_thread_name_by_id(
|
||||
_codex_home: &Path,
|
||||
_thread_id: &ThreadId,
|
||||
) -> io::Result<Option<String>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn find_thread_names_by_ids(
|
||||
_codex_home: &Path,
|
||||
_thread_ids: &HashSet<ThreadId>,
|
||||
) -> io::Result<HashMap<ThreadId, String>> {
|
||||
Ok(HashMap::new())
|
||||
}
|
||||
|
||||
pub async fn find_thread_path_by_name_str(
|
||||
_codex_home: &Path,
|
||||
_name: &str,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_session_init_error(
|
||||
err: &anyhow::Error,
|
||||
_codex_home: &Path,
|
||||
) -> crate::error::CodexErr {
|
||||
crate::error::CodexErr::Fatal(err.to_string())
|
||||
}
|
||||
|
||||
pub(crate) mod truncation {
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
|
||||
pub(crate) fn user_message_positions_in_rollout(_items: &[RolloutItem]) -> Vec<usize> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_rollout_before_nth_user_message_from_start(
|
||||
items: &[RolloutItem],
|
||||
_n_from_start: usize,
|
||||
) -> Vec<RolloutItem> {
|
||||
items.to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod metadata {
|
||||
pub(crate) use codex_rollout::metadata::builder_from_items;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use native::*;
|
||||
|
||||
pub mod policy {
|
||||
pub use codex_rollout::policy::*;
|
||||
}
|
||||
|
||||
pub mod recorder {
|
||||
pub use codex_rollout::recorder::*;
|
||||
}
|
||||
|
||||
pub mod session_index {
|
||||
pub use codex_rollout::session_index::*;
|
||||
}
|
||||
|
||||
pub(crate) use crate::session_rollout_init_error::map_session_init_error;
|
||||
|
||||
pub(crate) mod truncation {
|
||||
pub(crate) use crate::thread_rollout_truncation::*;
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm::*;
|
||||
|
||||
@@ -157,11 +157,17 @@ impl SessionStartupPrewarmHandle {
|
||||
|
||||
impl Session {
|
||||
pub(crate) async fn schedule_startup_prewarm(self: &Arc<Self>, base_instructions: String) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = base_instructions;
|
||||
return;
|
||||
}
|
||||
|
||||
let session_telemetry = self.services.session_telemetry.clone();
|
||||
let websocket_connect_timeout = self.provider().await.websocket_connect_timeout();
|
||||
let started_at = Instant::now();
|
||||
let startup_prewarm_session = Arc::clone(self);
|
||||
let startup_prewarm = tokio::spawn(async move {
|
||||
let startup_prewarm = crate::async_runtime::spawn(async move {
|
||||
let result =
|
||||
schedule_startup_prewarm_inner(startup_prewarm_session, base_instructions).await;
|
||||
let status = if result.is_ok() { "ready" } else { "failed" };
|
||||
|
||||
@@ -162,6 +162,16 @@ fn file_exists(path: &PathBuf) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn resolve_binary_path(binary_name: &str) -> Option<PathBuf> {
|
||||
which::which(binary_name).ok()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn resolve_binary_path(_binary_name: &str) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_shell_path(
|
||||
shell_type: ShellType,
|
||||
provided_path: Option<&PathBuf>,
|
||||
@@ -183,7 +193,7 @@ fn get_shell_path(
|
||||
return Some(default_shell_path);
|
||||
}
|
||||
|
||||
if let Ok(path) = which::which(binary_name) {
|
||||
if let Some(path) = resolve_binary_path(binary_name) {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
|
||||
45
codex-rs/core/src/shell_snapshot_wasm.rs
Normal file
45
codex-rs/core/src/shell_snapshot_wasm.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use crate::shell::Shell;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::ThreadId;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::watch;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShellSnapshot {
|
||||
pub path: PathBuf,
|
||||
pub cwd: PathBuf,
|
||||
}
|
||||
|
||||
impl ShellSnapshot {
|
||||
pub fn start_snapshotting(
|
||||
_codex_home: PathBuf,
|
||||
_session_id: ThreadId,
|
||||
_session_cwd: PathBuf,
|
||||
shell: &mut Shell,
|
||||
_session_telemetry: SessionTelemetry,
|
||||
) -> watch::Sender<Option<Arc<ShellSnapshot>>> {
|
||||
let (tx, rx) = watch::channel(None);
|
||||
shell.shell_snapshot = rx;
|
||||
tx
|
||||
}
|
||||
|
||||
pub fn refresh_snapshot(
|
||||
_codex_home: PathBuf,
|
||||
_session_id: ThreadId,
|
||||
_session_cwd: PathBuf,
|
||||
_shell: Shell,
|
||||
shell_snapshot_tx: watch::Sender<Option<Arc<ShellSnapshot>>>,
|
||||
_session_telemetry: SessionTelemetry,
|
||||
) {
|
||||
let _ = shell_snapshot_tx.send(None);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cleanup_stale_snapshots(
|
||||
_codex_home: &Path,
|
||||
_active_session_id: ThreadId,
|
||||
) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
30
codex-rs/core/src/spawn_wasm.rs
Normal file
30
codex-rs/core/src/spawn_wasm.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_DISABLED";
|
||||
pub const CODEX_SANDBOX_ENV_VAR: &str = "CODEX_SANDBOX";
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum StdioPolicy {
|
||||
RedirectForShellTool,
|
||||
Inherit,
|
||||
}
|
||||
|
||||
pub(crate) struct SpawnChildRequest<'a> {
|
||||
pub program: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
pub arg0: Option<&'a str>,
|
||||
pub cwd: PathBuf,
|
||||
pub network_sandbox_policy: NetworkSandboxPolicy,
|
||||
pub network: Option<&'a NetworkProxy>,
|
||||
pub stdio_policy: StdioPolicy,
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub(crate) async fn spawn_child_async(_request: SpawnChildRequest<'_>) -> std::io::Result<()> {
|
||||
Err(std::io::Error::other(
|
||||
"process spawning is unavailable in wasm",
|
||||
))
|
||||
}
|
||||
@@ -14,10 +14,10 @@ use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use rmcp::model::RequestId;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::codex::TurnContext;
|
||||
use crate::mcp_types::RequestId;
|
||||
use crate::protocol::ReviewDecision;
|
||||
use crate::protocol::TokenUsage;
|
||||
use crate::tasks::SessionTask;
|
||||
|
||||
@@ -1,21 +1,215 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use codex_rollout::state_db as rollout_state_db;
|
||||
pub use codex_rollout::state_db::StateDbHandle;
|
||||
pub use codex_rollout::state_db::apply_rollout_items;
|
||||
pub use codex_rollout::state_db::find_rollout_path_by_id;
|
||||
pub use codex_rollout::state_db::get_dynamic_tools;
|
||||
pub use codex_rollout::state_db::list_thread_ids_db;
|
||||
pub use codex_rollout::state_db::list_threads_db;
|
||||
pub use codex_rollout::state_db::mark_thread_memory_mode_polluted;
|
||||
pub use codex_rollout::state_db::normalize_cwd_for_state_db;
|
||||
pub use codex_rollout::state_db::open_if_present;
|
||||
pub use codex_rollout::state_db::persist_dynamic_tools;
|
||||
pub use codex_rollout::state_db::read_repair_rollout_path;
|
||||
pub use codex_rollout::state_db::reconcile_rollout;
|
||||
pub use codex_rollout::state_db::touch_thread_updated_at;
|
||||
pub use codex_state::LogEntry;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::StateDbHandle;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::apply_rollout_items;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::find_rollout_path_by_id;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::get_dynamic_tools;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::init;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::list_thread_ids_db;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::list_threads_db;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::mark_thread_memory_mode_polluted;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::normalize_cwd_for_state_db;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::open_if_present;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::persist_dynamic_tools;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::read_repair_rollout_path;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::reconcile_rollout;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_rollout::state_db::touch_thread_updated_at;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codex_state::LogEntry;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn get_state_db(config: &Config) -> Option<StateDbHandle> {
|
||||
rollout_state_db::get_state_db(config).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm {
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::rollout::list::Cursor;
|
||||
use crate::rollout::list::ThreadSortKey;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StateRuntimeStub;
|
||||
|
||||
pub type StateDbHandle = Arc<StateRuntimeStub>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LogEntry;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ThreadMetadataStub {
|
||||
pub agent_nickname: Option<String>,
|
||||
pub agent_role: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn init(_config: &Config) -> Option<StateDbHandle> {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn get_state_db(_config: &Config) -> Option<StateDbHandle> {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn open_if_present(
|
||||
_codex_home: &Path,
|
||||
_default_provider: &str,
|
||||
) -> Option<StateDbHandle> {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn find_rollout_path_by_id(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_thread_id: ThreadId,
|
||||
_archived_only: Option<bool>,
|
||||
_stage: &str,
|
||||
) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn get_dynamic_tools(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_thread_id: ThreadId,
|
||||
_stage: &str,
|
||||
) -> Option<Vec<DynamicToolSpec>> {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn list_thread_ids_db(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_codex_home: &Path,
|
||||
_page_size: usize,
|
||||
_cursor: Option<&Cursor>,
|
||||
_sort_key: ThreadSortKey,
|
||||
_allowed_sources: &[SessionSource],
|
||||
_model_providers: Option<&[String]>,
|
||||
_archived_only: bool,
|
||||
_stage: &str,
|
||||
) -> Option<Vec<ThreadId>> {
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn list_threads_db(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_codex_home: &Path,
|
||||
_page_size: usize,
|
||||
_cursor: Option<&Cursor>,
|
||||
_sort_key: ThreadSortKey,
|
||||
_allowed_sources: &[SessionSource],
|
||||
_model_providers: Option<&[String]>,
|
||||
_archived: bool,
|
||||
_search_term: Option<&str>,
|
||||
) -> Option<crate::rollout::list::ThreadsPage> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn normalize_cwd_for_state_db(cwd: &Path) -> PathBuf {
|
||||
cwd.to_path_buf()
|
||||
}
|
||||
|
||||
pub async fn persist_dynamic_tools(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_thread_id: ThreadId,
|
||||
_tools: Option<&[DynamicToolSpec]>,
|
||||
_stage: &str,
|
||||
) {
|
||||
}
|
||||
|
||||
pub async fn mark_thread_memory_mode_polluted(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_thread_id: ThreadId,
|
||||
_stage: &str,
|
||||
) {
|
||||
}
|
||||
|
||||
pub async fn reconcile_rollout(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_rollout_path: &Path,
|
||||
_default_provider: &str,
|
||||
_builder: Option<&()>,
|
||||
_items: &[RolloutItem],
|
||||
_archived_only: Option<bool>,
|
||||
_new_thread_memory_mode: Option<&str>,
|
||||
) {
|
||||
}
|
||||
|
||||
pub async fn read_repair_rollout_path(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_thread_id: Option<ThreadId>,
|
||||
_archived_only: Option<bool>,
|
||||
_rollout_path: &Path,
|
||||
) {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn apply_rollout_items(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_rollout_path: &Path,
|
||||
_default_provider: &str,
|
||||
_builder: Option<&()>,
|
||||
_items: &[RolloutItem],
|
||||
_stage: &str,
|
||||
_new_thread_memory_mode: Option<&str>,
|
||||
_updated_at_override: Option<DateTime<Utc>>,
|
||||
) {
|
||||
}
|
||||
|
||||
pub async fn touch_thread_updated_at(
|
||||
_context: Option<&StateRuntimeStub>,
|
||||
_thread_id: Option<ThreadId>,
|
||||
_updated_at: DateTime<Utc>,
|
||||
_stage: &str,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
impl StateRuntimeStub {
|
||||
pub async fn clear_memory_data(&self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn record_stage1_output_usage(
|
||||
&self,
|
||||
_thread_ids: &[ThreadId],
|
||||
) -> anyhow::Result<usize> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
pub async fn get_thread(
|
||||
&self,
|
||||
_id: ThreadId,
|
||||
) -> anyhow::Result<Option<ThreadMetadataStub>> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm::*;
|
||||
|
||||
@@ -115,9 +115,9 @@ async fn save_image_generation_result(
|
||||
})?;
|
||||
let path = image_generation_artifact_path(codex_home, session_id, call_id);
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
crate::async_fs::create_dir_all(parent).await?;
|
||||
}
|
||||
tokio::fs::write(&path, bytes).await?;
|
||||
crate::async_fs::write(&path, bytes).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ use tokio_util::sync::CancellationToken;
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct CompactTask;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl SessionTask for CompactTask {
|
||||
fn kind(&self) -> TaskKind {
|
||||
TaskKind::Compact
|
||||
|
||||
@@ -26,7 +26,8 @@ pub(crate) struct GhostSnapshotTask {
|
||||
|
||||
const SNAPSHOT_WARNING_THRESHOLD: Duration = Duration::from_secs(240);
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl SessionTask for GhostSnapshotTask {
|
||||
fn kind(&self) -> TaskKind {
|
||||
TaskKind::Regular
|
||||
@@ -43,7 +44,7 @@ impl SessionTask for GhostSnapshotTask {
|
||||
_input: Vec<UserInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
tokio::task::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
let token = self.token;
|
||||
let warnings_enabled = !ctx.ghost_snapshot.disable_warnings;
|
||||
// Channel used to signal when the snapshot work has finished so the
|
||||
@@ -56,7 +57,7 @@ impl SessionTask for GhostSnapshotTask {
|
||||
// Fire a generic warning if the snapshot is still running after
|
||||
// three minutes; this helps users discover large untracked files
|
||||
// that might need to be added to .gitignore.
|
||||
tokio::task::spawn(async move {
|
||||
crate::async_runtime::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(SNAPSHOT_WARNING_THRESHOLD) => {
|
||||
session_for_warning.session
|
||||
|
||||
@@ -7,7 +7,6 @@ mod user_shell;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::select;
|
||||
@@ -29,6 +28,7 @@ use crate::hook_runtime::inspect_pending_input;
|
||||
use crate::hook_runtime::record_additional_contexts;
|
||||
use crate::hook_runtime::record_pending_input;
|
||||
use crate::models_manager::manager::ModelsManager;
|
||||
use crate::monotonic_time::Instant;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::TurnAbortReason;
|
||||
use crate::protocol::TurnAbortedEvent;
|
||||
@@ -125,7 +125,8 @@ impl SessionTaskContext {
|
||||
/// intentionally small: implementers identify themselves via
|
||||
/// [`SessionTask::kind`], perform their work in [`SessionTask::run`], and may
|
||||
/// release resources in [`SessionTask::abort`].
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
pub(crate) trait SessionTask: Send + Sync + 'static {
|
||||
/// Describes the type of work the task performs so the session can
|
||||
/// surface it in telemetry and UI.
|
||||
@@ -227,7 +228,7 @@ impl Session {
|
||||
turn.id = %turn_context.sub_id,
|
||||
model = %turn_context.model_info.slug,
|
||||
);
|
||||
let handle = tokio::spawn(
|
||||
let handle = crate::async_runtime::spawn(
|
||||
async move {
|
||||
let ctx_for_finish = Arc::clone(&ctx);
|
||||
let last_agent_message = task_for_run
|
||||
@@ -456,11 +457,16 @@ impl Session {
|
||||
|
||||
if should_clear_active_turn {
|
||||
let session = Arc::clone(self);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _scheduler = tokio::task::spawn_blocking(move || {
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
session.maybe_start_turn_for_pending_work().await;
|
||||
});
|
||||
});
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _scheduler = crate::async_runtime::spawn(async move {
|
||||
session.maybe_start_turn_for_pending_work().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ impl RegularTask {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl SessionTask for RegularTask {
|
||||
fn kind(&self) -> TaskKind {
|
||||
TaskKind::Regular
|
||||
|
||||
@@ -48,7 +48,8 @@ impl ReviewTask {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl SessionTask for ReviewTask {
|
||||
fn kind(&self) -> TaskKind {
|
||||
TaskKind::Review
|
||||
|
||||
@@ -25,7 +25,8 @@ impl UndoTask {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl SessionTask for UndoTask {
|
||||
fn kind(&self) -> TaskKind {
|
||||
TaskKind::Regular
|
||||
|
||||
@@ -62,7 +62,8 @@ impl UserShellCommandTask {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl SessionTask for UserShellCommandTask {
|
||||
fn kind(&self) -> TaskKind {
|
||||
TaskKind::Regular
|
||||
|
||||
@@ -86,6 +86,12 @@ impl Drop for TempCodexHomeGuard {
|
||||
}
|
||||
|
||||
fn build_skills_watcher(skills_manager: Arc<SkillsManager>) -> Arc<SkillsWatcher> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = skills_manager;
|
||||
return Arc::new(SkillsWatcher::noop());
|
||||
}
|
||||
|
||||
if should_use_test_thread_manager_behavior()
|
||||
&& let Ok(handle) = Handle::try_current()
|
||||
&& handle.runtime_flavor() == RuntimeFlavor::CurrentThread
|
||||
@@ -447,6 +453,27 @@ impl ThreadManager {
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
parent_trace,
|
||||
/*code_mode_runtime*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn start_thread_with_code_mode_runtime(
|
||||
&self,
|
||||
config: Config,
|
||||
code_mode_runtime: Arc<dyn crate::CodeModeRuntime>,
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.state.spawn_thread(
|
||||
config,
|
||||
InitialHistory::New,
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
Vec::new(),
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
Some(code_mode_runtime),
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -487,6 +514,7 @@ impl ThreadManager {
|
||||
persist_extended_history,
|
||||
/*metrics_service_name*/ None,
|
||||
parent_trace,
|
||||
/*code_mode_runtime*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -506,6 +534,7 @@ impl ThreadManager {
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
/*code_mode_runtime*/ None,
|
||||
/*user_shell_override*/ Some(user_shell_override),
|
||||
))
|
||||
.await
|
||||
@@ -528,6 +557,7 @@ impl ThreadManager {
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
/*code_mode_runtime*/ None,
|
||||
/*user_shell_override*/ Some(user_shell_override),
|
||||
))
|
||||
.await
|
||||
@@ -635,6 +665,7 @@ impl ThreadManager {
|
||||
persist_extended_history,
|
||||
/*metrics_service_name*/ None,
|
||||
parent_trace,
|
||||
/*code_mode_runtime*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -736,6 +767,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
/*code_mode_runtime*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -763,6 +795,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
/*code_mode_runtime*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -791,6 +824,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
/*code_mode_runtime*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -808,6 +842,7 @@ impl ThreadManagerState {
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
code_mode_runtime: Option<Arc<dyn codex_code_mode::CodeModeRuntime>>,
|
||||
user_shell_override: Option<crate::shell::Shell>,
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.spawn_thread_with_source(
|
||||
@@ -822,6 +857,7 @@ impl ThreadManagerState {
|
||||
/*inherited_shell_snapshot*/ None,
|
||||
/*inherited_exec_policy*/ None,
|
||||
parent_trace,
|
||||
code_mode_runtime,
|
||||
user_shell_override,
|
||||
))
|
||||
.await
|
||||
@@ -841,6 +877,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
code_mode_runtime: Option<Arc<dyn codex_code_mode::CodeModeRuntime>>,
|
||||
user_shell_override: Option<crate::shell::Shell>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let watch_registration = self.skills_watcher.register_config(
|
||||
@@ -868,6 +905,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
user_shell_override,
|
||||
code_mode_runtime,
|
||||
parent_trace,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::monotonic_time::Instant;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
@@ -32,7 +33,7 @@ impl CodeModeExecuteHandler {
|
||||
.code_mode_service
|
||||
.stored_values()
|
||||
.await;
|
||||
let started_at = std::time::Instant::now();
|
||||
let started_at = Instant::now();
|
||||
let response = exec
|
||||
.session
|
||||
.services
|
||||
@@ -53,7 +54,8 @@ impl CodeModeExecuteHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl ToolHandler for CodeModeExecuteHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_code_mode::CodeModeRuntime;
|
||||
use codex_code_mode::CodeModeTurnHost;
|
||||
use codex_code_mode::CodeModeTurnWorkerHandle;
|
||||
use codex_code_mode::RuntimeResponse;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
@@ -18,6 +20,7 @@ use crate::client_common::tools::ToolSpec;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::monotonic_time::Instant;
|
||||
use crate::tools::ToolRouter;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::SharedTurnDiffTracker;
|
||||
@@ -48,14 +51,16 @@ pub(crate) struct ExecContext {
|
||||
}
|
||||
|
||||
pub(crate) struct CodeModeService {
|
||||
inner: codex_code_mode::CodeModeService,
|
||||
inner: Arc<dyn CodeModeRuntime>,
|
||||
}
|
||||
|
||||
impl CodeModeService {
|
||||
pub(crate) fn new(_js_repl_node_path: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
inner: codex_code_mode::CodeModeService::new(),
|
||||
}
|
||||
Self::from_runtime(Arc::new(codex_code_mode::CodeModeService::new()))
|
||||
}
|
||||
|
||||
pub(crate) fn from_runtime(inner: Arc<dyn CodeModeRuntime>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub(crate) async fn stored_values(&self) -> std::collections::HashMap<String, JsonValue> {
|
||||
@@ -89,7 +94,7 @@ impl CodeModeService {
|
||||
turn: &Arc<TurnContext>,
|
||||
router: Arc<ToolRouter>,
|
||||
tracker: SharedTurnDiffTracker,
|
||||
) -> Option<codex_code_mode::CodeModeTurnWorker> {
|
||||
) -> Option<Box<dyn CodeModeTurnWorkerHandle>> {
|
||||
if !turn.features.enabled(Feature::CodeMode) {
|
||||
return None;
|
||||
}
|
||||
@@ -110,7 +115,8 @@ struct CoreTurnHost {
|
||||
tool_runtime: ToolCallRuntime,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
impl CodeModeTurnHost for CoreTurnHost {
|
||||
async fn invoke_tool(
|
||||
&self,
|
||||
@@ -151,7 +157,7 @@ pub(super) async fn handle_runtime_response(
|
||||
exec: &ExecContext,
|
||||
response: RuntimeResponse,
|
||||
max_output_tokens: Option<usize>,
|
||||
started_at: std::time::Instant,
|
||||
started_at: Instant,
|
||||
) -> Result<FunctionToolOutput, String> {
|
||||
let script_status = format_script_status(&response);
|
||||
|
||||
@@ -243,6 +249,12 @@ fn truncate_code_mode_result(
|
||||
pub(super) async fn build_enabled_tools(
|
||||
exec: &ExecContext,
|
||||
) -> Vec<codex_code_mode::ToolDefinition> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = exec;
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let router = build_nested_router(exec).await;
|
||||
let mut out = router
|
||||
.specs()
|
||||
|
||||
@@ -2,6 +2,7 @@ use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::monotonic_time::Instant;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
@@ -39,7 +40,8 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl ToolHandler for CodeModeWaitHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
@@ -60,7 +62,7 @@ impl ToolHandler for CodeModeWaitHandler {
|
||||
ToolPayload::Function { arguments } if tool_name == WAIT_TOOL_NAME => {
|
||||
let args: ExecWaitArgs = parse_arguments(&arguments)?;
|
||||
let exec = ExecContext { session, turn };
|
||||
let started_at = std::time::Instant::now();
|
||||
let started_at = Instant::now();
|
||||
let response = exec
|
||||
.session
|
||||
.services
|
||||
|
||||
@@ -130,7 +130,8 @@ async fn effective_patch_permissions(
|
||||
)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl ToolHandler for ApplyPatchHandler {
|
||||
type Output = ApplyPatchToolOutput;
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ use tracing::warn;
|
||||
|
||||
pub struct DynamicToolHandler;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl ToolHandler for DynamicToolHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
|
||||
pub struct McpHandler;
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl ToolHandler for McpHandler {
|
||||
type Output = CallToolResult;
|
||||
|
||||
|
||||
@@ -6,13 +6,6 @@ use std::time::Instant;
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::function_call_output_content_items_to_text;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
use rmcp::model::PaginatedRequestParams;
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
use rmcp::model::ReadResourceResult;
|
||||
use rmcp::model::Resource;
|
||||
use rmcp::model::ResourceTemplate;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -21,6 +14,13 @@ use serde_json::Value;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::mcp_types::ListResourceTemplatesResult;
|
||||
use crate::mcp_types::ListResourcesResult;
|
||||
use crate::mcp_types::PaginatedRequestParams;
|
||||
use crate::mcp_types::ReadResourceRequestParams;
|
||||
use crate::mcp_types::ReadResourceResult;
|
||||
use crate::mcp_types::Resource;
|
||||
use crate::mcp_types::ResourceTemplate;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::McpInvocation;
|
||||
use crate::protocol::McpToolCallBeginEvent;
|
||||
@@ -178,7 +178,8 @@ struct ReadResourcePayload {
|
||||
result: ReadResourceResult,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
impl ToolHandler for McpResourceHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod agent_jobs;
|
||||
pub mod apply_patch;
|
||||
mod dynamic;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod js_repl;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod list_dir;
|
||||
mod mcp;
|
||||
mod mcp_resource;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod multi_agents;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod multi_agents_common;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod multi_agents_v2;
|
||||
mod plan;
|
||||
mod request_permissions;
|
||||
mod request_user_input;
|
||||
mod shell;
|
||||
mod test_sync;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod tool_search;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod tool_suggest;
|
||||
pub(crate) mod unified_exec;
|
||||
mod view_image;
|
||||
@@ -37,11 +45,16 @@ pub use apply_patch::ApplyPatchHandler;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
pub use dynamic::DynamicToolHandler;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use js_repl::JsReplHandler;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use js_repl::JsReplResetHandler;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use list_dir::ListDirHandler;
|
||||
pub use mcp::McpHandler;
|
||||
pub use mcp_resource::McpResourceHandler;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS;
|
||||
pub use plan::PlanHandler;
|
||||
pub use request_permissions::RequestPermissionsHandler;
|
||||
pub(crate) use request_permissions::request_permissions_tool_description;
|
||||
@@ -50,10 +63,31 @@ pub(crate) use request_user_input::request_user_input_tool_description;
|
||||
pub use shell::ShellCommandHandler;
|
||||
pub use shell::ShellHandler;
|
||||
pub use test_sync::TestSyncHandler;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) const DEFAULT_WAIT_TIMEOUT_MS: u64 = 30_000;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use multi_agents_common::MAX_WAIT_TIMEOUT_MS;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) const MAX_WAIT_TIMEOUT_MS: u64 = 3_600_000;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use multi_agents_common::MIN_WAIT_TIMEOUT_MS;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) const MIN_WAIT_TIMEOUT_MS: u64 = 10_000;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use tool_search::DEFAULT_LIMIT as TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) const TOOL_SEARCH_DEFAULT_LIMIT: usize = 8;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use tool_search::TOOL_SEARCH_TOOL_NAME;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tool_search::ToolSearchHandler;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use tool_suggest::TOOL_SUGGEST_TOOL_NAME;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest";
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tool_suggest::ToolSuggestHandler;
|
||||
pub use unified_exec::UnifiedExecHandler;
|
||||
pub use view_image::ViewImageHandler;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user