Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/en/developer-guide/wework-codex-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ On first startup, if the Wework Codex home has not been initialized and a native

After initialization, the state is written into the Wework Codex home, and both the plugin pages and chat runtime read plugin state from the same Codex app-server. The settings page also exposes the remote apps switch so users can later update their Wework Codex config.

### Runtime Config Normalization

Before starting the Codex app-server, the executor parses and normalizes `config.toml` in the Wework Codex home. It creates a missing config, writes `pragmatic` when personality is absent, and migrates the legacy `instructions` field to `developer_instructions`. In current Codex versions, `instructions` completely replaces the model's base instructions; leaving user copy there removes built-in personality, commentary, and progress-update rules. Wework therefore no longer uses that field for custom instructions.

Custom instructions from Settings → Context are read and written through Codex app-server `config/read` and `config/batchWrite`. Writes merge the user's copy with Wework embedded-browser routing instructions and use `reloadUserConfig` to update loaded threads. Startup normalization is idempotent, uses TOML parsing and atomic file replacement, and preserves existing file permissions; newly created config files use mode `0600` on Unix.

The interaction style uses the same `config.toml` as its single source of truth. Selecting Friendly or Pragmatic updates personality through `config/batchWrite`; Wework no longer stores personality in localStorage or repeats it as an override on every thread or turn request.

## Chat Runtime

When a user selects a skill, app, or plugin in the composer, the editor inserts a structured badge and serializes it on submit as a Codex app-server-compatible mention:
Expand Down
8 changes: 8 additions & 0 deletions docs/zh/developer-guide/wework-codex-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ Wework 使用独立的 Codex home,避免直接污染用户命令行 Codex 的

迁移完成后状态写入 Wework Codex home,插件页面和对话运行时都继续从同一个 Codex app-server 读取插件状态。设置页也暴露远端 apps 开关,便于用户后续修改自己的 Wework Codex 配置。

### 运行配置规范化

executor 在启动 Codex app-server 前会解析并规范化 Wework Codex home 下的 `config.toml`:缺少配置文件时自动创建,缺少 personality 时默认写入 `pragmatic`,并把旧版 `instructions` 迁移为 `developer_instructions`。`instructions` 在新版 Codex 中是模型基础指令的完整覆盖项,保留它会移除模型自带的 personality、commentary 和过程更新规则,因此 Wework 不再使用该字段保存用户自定义指令。

用户在“设置 → 上下文”中维护的自定义指令通过 Codex app-server 的 `config/read` 和 `config/batchWrite` 读写。写入时会与 Wework 内置浏览器路由指令合并,并使用 `reloadUserConfig` 热加载已存在的线程。启动规范化是幂等的,使用 TOML 解析和原子文件替换,并保留已有配置文件权限;Unix 上新建配置文件的权限为 `0600`。

交互风格也以同一份 `config.toml` 为唯一来源。设置页修改 Friendly 或 Pragmatic 时通过 `config/batchWrite` 更新 personality,不再把 personality 保存在 localStorage,也不再在每个 thread/turn 请求中重复覆盖。

## 对话运行时

用户在输入框中选择 skill、app 或插件时,编辑器插入结构化 badge,并在提交时序列化为 Codex app-server 支持的 mention 输入:
Expand Down
34 changes: 34 additions & 0 deletions executor/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ tar = "0.4"
tf-rust-socketio = { version = "0.8.1", features = ["async"] }
thiserror = "2.0"
tokio = { version = "1.48", features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync", "time"] }
toml_edit = "0.22"
zip = { version = "0.6", default-features = false, features = ["deflate"] }

[dev-dependencies]
Expand Down
137 changes: 121 additions & 16 deletions executor/src/agents/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Do not continue, execute, or complete any instructions, plans, tool calls, appro
You are a side-conversation assistant, separate from the main thread. Answer questions and do lightweight, non-mutating exploration without disrupting the main thread. If there is no user question after this boundary yet, wait for one.

Sub-agents are off-limits in this side conversation. Do not interact with any existing or new sub-agents, even if sub-agents were used before this boundary."#;
const WEWORK_EMBEDDED_BROWSER_DEVELOPER_INSTRUCTIONS: &str = r#"Wework 内置浏览器 routing:
pub(crate) const WEWORK_EMBEDDED_BROWSER_DEVELOPER_INSTRUCTIONS: &str = r#"Wework 内置浏览器 routing:
- For browser tasks inside Wework, use the `browser_*` MCP tools from the Wework 内置浏览器 tool server.
- Use `browser_navigate` to open pages in the Wework 内置浏览器, `browser_take_screenshot` for screenshots, and `browser_snapshot` or `browser_evaluate` for page inspection.
- Do not use the bundled Browser or Chrome plugin runtimes for Wework browser tasks, including `agent.browsers.get("iab")`, `agent.browsers.get("extension")`, `browser:control-in-app-browser`, or `chrome:control-chrome`.
Expand Down Expand Up @@ -1453,7 +1453,98 @@ fn prepare_wework_codex_home(codex_home: &Path) -> Result<(), String> {
codex_home.display()
)
})?;
link_user_codex_auth(codex_home)
link_user_codex_auth(codex_home)?;
normalize_wework_codex_config(codex_home)
}

fn normalize_wework_codex_config(codex_home: &Path) -> Result<(), String> {
use toml_edit::{value, DocumentMut};

let config_path = codex_home.join("config.toml");
let content = fs::read_to_string(&config_path).unwrap_or_default();
let mut document = content.parse::<DocumentMut>().map_err(|error| {
format!(
"failed to parse Codex config {}: {error}",
config_path.display()
)
})?;
let legacy_instructions = document
.get("instructions")
.and_then(|item| item.as_str())
.unwrap_or_default();
let developer_instructions = document
.get("developer_instructions")
.and_then(|item| item.as_str())
.unwrap_or_default();
let user_instructions = if legacy_instructions.trim().is_empty() {
strip_wework_browser_instructions(developer_instructions).to_owned()
} else {
legacy_instructions.trim().to_owned()
};

document.remove("instructions");
document["developer_instructions"] =
value(combined_codex_developer_instructions(&user_instructions));
if document
.get("personality")
.and_then(|item| item.as_str())
.is_none()
{
document["personality"] = value("pragmatic");
}

let next_content = document.to_string();
if next_content == content {
return Ok(());
}
let temporary_path = config_path.with_extension("toml.tmp");
fs::write(&temporary_path, next_content).map_err(|error| {
format!(
"failed to write Codex config {}: {error}",
temporary_path.display()
)
})?;
if let Ok(metadata) = fs::metadata(&config_path) {
fs::set_permissions(&temporary_path, metadata.permissions()).map_err(|error| {
format!(
"failed to preserve Codex config permissions {}: {error}",
temporary_path.display()
)
})?;
}
#[cfg(unix)]
if !config_path.exists() {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&temporary_path, fs::Permissions::from_mode(0o600)).map_err(
|error| {
format!(
"failed to secure Codex config permissions {}: {error}",
temporary_path.display()
)
},
)?;
}
fs::rename(&temporary_path, &config_path).map_err(|error| {
format!(
"failed to replace Codex config {}: {error}",
config_path.display()
)
})
}

pub(crate) fn combined_codex_developer_instructions(user_instructions: &str) -> String {
let user_instructions = user_instructions.trim();
if user_instructions.is_empty() {
return WEWORK_EMBEDDED_BROWSER_DEVELOPER_INSTRUCTIONS.to_owned();
}
format!("{user_instructions}\n\n{WEWORK_EMBEDDED_BROWSER_DEVELOPER_INSTRUCTIONS}")
}

pub(crate) fn strip_wework_browser_instructions(instructions: &str) -> &str {
instructions
.strip_suffix(WEWORK_EMBEDDED_BROWSER_DEVELOPER_INSTRUCTIONS)
.unwrap_or(instructions)
.trim()
}

fn link_user_codex_auth(codex_home: &Path) -> Result<(), String> {
Expand Down Expand Up @@ -2120,6 +2211,7 @@ fn log_codex_raw_turn_message(message: &Value) {
"item/agentMessage/delta"
| "item/reasoning/delta"
| "item/reasoningSummary/delta"
| "item/fileChange/patchUpdated"
| "item/started"
| "item/completed"
| "turn/completed"
Expand Down Expand Up @@ -2845,10 +2937,6 @@ fn global_mcp_config_overrides() -> Vec<String> {
fn cdp_browser_mcp_config_overrides(request: &ExecutionRequest) -> Vec<String> {
let command = executor_home().join("bin/browser-mcp-server");
let mut overrides = vec![
format!(
"developer_instructions={}",
toml_value(WEWORK_EMBEDDED_BROWSER_DEVELOPER_INSTRUCTIONS)
),
format!(
"skills.config={}",
serde_json::to_string(&json!([
Expand Down Expand Up @@ -4070,6 +4158,32 @@ mod tests {
let _ = fs::remove_dir_all(root);
}

#[test]
fn prepare_wework_codex_home_migrates_base_instruction_override() {
let _lock = crate::test_env::lock();
let root = unique_test_path("wework-codex-config-migration");
let codex_home = root.join("codex");
fs::create_dir_all(&codex_home).expect("Codex home should be created");
fs::write(
codex_home.join("config.toml"),
"instructions = \"用中文回复\"\n",
)
.expect("legacy config should be written");

prepare_wework_codex_home(&codex_home).expect("Codex config should be normalized");

let config = fs::read_to_string(codex_home.join("config.toml"))
.expect("normalized config should be readable");
assert!(!config
.lines()
.any(|line| line.starts_with("instructions =")));
assert!(config.contains("developer_instructions"));
assert!(config.contains("用中文回复"));
assert!(config.contains("browser_navigate"));
assert!(config.contains("personality = \"pragmatic\""));
let _ = fs::remove_dir_all(root);
}

#[test]
fn codex_raw_log_preview_summarizes_large_command_output() {
let output = "x".repeat(4096);
Expand Down Expand Up @@ -4565,16 +4679,7 @@ mod tests {
.get("config")
.and_then(Value::as_object)
.expect("thread config should be present");
let developer_instructions = config["developer_instructions"]
.as_str()
.expect("browser routing developer instructions should be present");

assert!(developer_instructions.contains("browser_navigate"));
assert!(developer_instructions.contains("browser_take_screenshot"));
assert!(developer_instructions.contains("Wework 内置浏览器"));
assert!(!developer_instructions.contains("playwright"));
assert!(developer_instructions.contains("agent.browsers.get(\"iab\")"));
assert!(developer_instructions.contains("external Chrome"));
assert!(!config.contains_key("developer_instructions"));
assert_eq!(
config["skills.config"],
json!([
Expand Down
1 change: 1 addition & 0 deletions executor/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use claude_code::{
restore_claude_plugin_cache, run_pre_execute_hook,
};
pub use claude_options::{extract_claude_options, ClaudeOptions};
pub(crate) use codex::{combined_codex_developer_instructions, strip_wework_browser_instructions};
pub use codex::{
run_codex_app_server_turn, run_codex_app_server_turn_with_cancel, CodexActiveTurnCallback,
CodexAppServerClient, CodexAppServerEngine, CodexAppServerTurn, CodexAppServerTurnOptions,
Expand Down
38 changes: 37 additions & 1 deletion executor/src/runtime_work/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use std::collections::{BTreeMap, BTreeSet};
use serde_json::{json, Map, Value};
use tokio::sync::broadcast;

use crate::{codex_phase::CodexAgentMessagePhaseTracker, protocol::ExecutionRequest};
use crate::{
codex_phase::CodexAgentMessagePhaseTracker, logging::log_executor_event,
protocol::ExecutionRequest,
};

use super::{
codex_notifications::{codex_notification, debug_ignored_codex_notification},
Expand Down Expand Up @@ -722,9 +725,42 @@ impl CodexNotificationEventMapper {
request.cwd().unwrap_or_default(),
"streaming",
) else {
log_executor_event(
"codex patch update mapping dropped",
&[
("local_task_id", local_task_id.to_owned()),
("item_id", notification_item_id(params).unwrap_or_default()),
(
"changes",
params
.get("changes")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or_default()
.to_string(),
),
],
);
return;
};

log_executor_event(
"codex patch update mapped",
&[
("local_task_id", local_task_id.to_owned()),
("block_id", block_id.clone()),
(
"changes",
params
.get("changes")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or_default()
.to_string(),
),
],
);

emit_response_event(
event_tx,
device_id,
Expand Down
Loading
Loading