mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix: trust-gate project hooks and exec policies (#14718)
## Summary - trust-gate project `.codex` layers consistently, including repos that have `.codex/hooks.json` or `.codex/execpolicy/*.rules` but no `.codex/config.toml` - keep disabled project layers in the config stack so nested trusted project layers still resolve correctly, while preventing hooks and exec policies from loading until the project is trusted - update app-server/TUI onboarding copy to make the trust boundary explicit and add regressions for loader, hooks, exec-policy, and onboarding coverage ## Security Before this change, an untrusted repo could auto-load project hooks or exec policies from `.codex/` as long as `config.toml` was absent. This makes trust the single gate for project-local config, hooks, and exec policies. ## Stack - Parent of #15936 ## Test - cargo test -p codex-core without_config_toml --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -8,6 +8,7 @@ use crate::config_loader::NetworkDomainPermissionToml;
|
||||
use crate::config_loader::NetworkDomainPermissionsToml;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::config_loader::project_trust_key;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::shell::default_user_shell;
|
||||
@@ -21,6 +22,7 @@ use codex_models_manager::bundled_models_response;
|
||||
use codex_models_manager::model_info;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
@@ -53,6 +55,8 @@ use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::router::ToolCallSource;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::NetworkRuleProtocol;
|
||||
use codex_execpolicy::Policy;
|
||||
@@ -303,6 +307,75 @@ fn user_input_texts(items: &[ResponseItem]) -> Vec<&str> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn write_project_hooks(dot_codex: &Path) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(dot_codex)?;
|
||||
std::fs::write(
|
||||
dot_codex.join("hooks.json"),
|
||||
r#"{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo hello from hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
}
|
||||
|
||||
async fn write_project_trust_config(
|
||||
codex_home: &Path,
|
||||
trusted_projects: &[(&Path, TrustLevel)],
|
||||
) -> std::io::Result<()> {
|
||||
tokio::fs::write(
|
||||
codex_home.join(codex_config::CONFIG_TOML_FILE),
|
||||
toml::to_string(&ConfigToml {
|
||||
projects: Some(
|
||||
trusted_projects
|
||||
.iter()
|
||||
.map(|(project, trust_level)| {
|
||||
(
|
||||
project_trust_key(project),
|
||||
ProjectConfig {
|
||||
trust_level: Some(*trust_level),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<std::collections::HashMap<_, _>>(),
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("serialize config"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn preview_session_start_hooks(
|
||||
config: &crate::config::Config,
|
||||
) -> std::io::Result<Vec<codex_protocol::protocol::HookRunSummary>> {
|
||||
let hooks = Hooks::new(HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(config.config_layer_stack.clone()),
|
||||
..HooksConfig::default()
|
||||
});
|
||||
|
||||
Ok(
|
||||
hooks.preview_session_start(&codex_hooks::SessionStartRequest {
|
||||
session_id: ThreadId::new(),
|
||||
cwd: config.cwd.clone(),
|
||||
transcript_path: None,
|
||||
model: "gpt-5".to_string(),
|
||||
permission_mode: "default".to_string(),
|
||||
source: codex_hooks::SessionStartSource::Startup,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> ToolCallRuntime {
|
||||
let router = Arc::new(ToolRouter::from_config(
|
||||
&turn_context.tools_config,
|
||||
@@ -6044,3 +6117,85 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
|
||||
|
||||
pretty_assertions::assert_eq!(output, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_start_hooks_only_load_from_trusted_project_layers() -> std::io::Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let codex_home = temp.path().join("home");
|
||||
let project_root = temp.path().join("project");
|
||||
let nested = project_root.join("nested");
|
||||
let root_dot_codex = project_root.join(".codex");
|
||||
let nested_dot_codex = nested.join(".codex");
|
||||
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
std::fs::create_dir_all(&nested_dot_codex)?;
|
||||
std::fs::write(project_root.join(".git"), "gitdir: here")?;
|
||||
write_project_hooks(&root_dot_codex)?;
|
||||
write_project_hooks(&nested_dot_codex)?;
|
||||
write_project_trust_config(&codex_home, &[(&nested, TrustLevel::Trusted)]).await?;
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.fallback_cwd(Some(nested))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let preview = preview_session_start_hooks(&config).await?;
|
||||
let expected_source_path = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(
|
||||
nested_dot_codex.join("hooks.json"),
|
||||
)?;
|
||||
assert_eq!(
|
||||
preview
|
||||
.iter()
|
||||
.map(|run| &run.source_path)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![&expected_source_path],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_start_hooks_require_project_trust_without_config_toml() -> std::io::Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let project_root = temp.path().join("project");
|
||||
let nested = project_root.join("nested");
|
||||
let dot_codex = project_root.join(".codex");
|
||||
std::fs::create_dir_all(&nested)?;
|
||||
std::fs::write(project_root.join(".git"), "gitdir: here")?;
|
||||
write_project_hooks(&dot_codex)?;
|
||||
|
||||
let cases = [
|
||||
("unknown", Vec::<(&Path, TrustLevel)>::new(), 0_usize),
|
||||
(
|
||||
"untrusted",
|
||||
vec![(&project_root as &Path, TrustLevel::Untrusted)],
|
||||
0_usize,
|
||||
),
|
||||
(
|
||||
"trusted",
|
||||
vec![(&project_root as &Path, TrustLevel::Trusted)],
|
||||
1_usize,
|
||||
),
|
||||
];
|
||||
|
||||
for (name, trust_entries, expected_hooks) in cases {
|
||||
let codex_home = temp.path().join(format!("home_{name}"));
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
write_project_trust_config(&codex_home, &trust_entries).await?;
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.fallback_cwd(Some(nested.clone()))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
preview_session_start_hooks(&config).await?.len(),
|
||||
expected_hooks,
|
||||
"unexpected hook count for {name}",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user