Skip to content

ExecPolicy语言与规则

从可信配置层、Starlark规则、prefix与host executable匹配走到复合命令聚合、模型视图和policy amendment持久化。

基于rust-v0.150.0
CodexRustSecurityExecPolicy

ExecPolicy语言与规则 ​

ExecPolicy 不是 shell 字符串黑名单,也不是“找到第一条规则就停止”。规则文件用受限 Starlark builtin 构造 token-level prefix、network host 和 host executable 映射;运行时先把 shell 输入拆成多个命令段,再收集每一段的显式规则或 heuristic fallback,最终取最严格 decision。

安全性还取决于规则从哪里加载。项目仓库可以包含 .rules 文件,但只有进入可信配置层的规则才参与执行;requirements policy 作为高优先级 overlay 合并,即使用户规则解析失败也会保留。针对某些模型,Core 还会过滤 Allow prefix,同时保留 Prompt、Forbidden、network 与 executable 映射。

本文承接PermissionProfile解析和ApprovalPolicy完整参考。前文解释权限与提示;本文追踪 policy source、语言、匹配、聚合、Core 映射和 amendment。

1. 可信来源 ​

Core 按配置层从低到高扫描每层的 rules/ 目录,只读取普通 .rules 文件。已被 trust decision 禁用的 project layer 不会出现在正常可信视图;requirements 还可以显式忽略 user/project rules,同时保留 system/managed 层。

源码位置:codex-rs/core/src/exec_policy.rs :: load_exec_policy

rust
let mut policy_paths = Vec::new();
for layer in config_stack.layers_low_to_high() {
    if config_stack.ignore_user_and_project_exec_policy_rules()
        && matches!(
            layer.name,
            ConfigLayerSource::User { .. } | ConfigLayerSource::Project { .. }
        )
    {
        continue;
    }
    if let Some(config_folder) = layer.config_folder() {
        let policy_dir = config_folder.join(RULES_DIR_NAME);
        let layer_policy_paths = collect_policy_files(&policy_dir).await?;
        policy_paths.extend(layer_policy_paths);
    }
}

文件按路径排序后依次解析。每层优先级不是通过“删除低层规则”实现,而是把所有规则放入 policy;同一 executable 的多条 prefix rule 可以并存,最终匹配时再取最严格结果。host executable 同名映射则由高层覆盖低层。

源码位置:codex-rs/core/src/exec_policy.rs :: load_exec_policy

rust
let mut parser = PolicyParser::new();
for policy_path in &policy_paths {
    let contents = fs::read_to_string(policy_path)
        .await
        .map_err(|source| ExecPolicyError::ReadFile {
            path: policy_path.clone(),
            source,
        })?;
    let identifier = policy_path.to_string_lossy().to_string();
    parser
        .parse(&identifier, &contents)
        .map_err(|source| ExecPolicyError::ParsePolicy {
            path: identifier,
            source,
        })?;
}

let policy = parser.build();
let Some(requirements_policy) = config_stack.requirements().exec_policy.as_deref() else {
    return Ok(policy);
};

Ok(policy.merge_overlay(requirements_policy.as_ref()))

若某个普通 policy 文件解析失败,load_exec_policy_with_warning 不会返回之前成功解析的半份用户 policy;它回退为空 policy 或 requirements policy,并返回 warning。managed requirements 因而不会被 malformed user rule 清空。

源码位置:codex-rs/core/src/exec_policy.rs :: load_exec_policy_with_warning

rust
match load_exec_policy(config_stack).await {
    Ok(policy) => Ok((policy, None)),
    Err(err @ ExecPolicyError::ParsePolicy { .. }) => {
        let policy = config_stack
            .requirements()
            .exec_policy
            .as_deref()
            .map_or_else(Policy::empty, |policy| policy.as_ref().clone());
        Ok((policy, Some(err)))
    }
    Err(err) => Err(err),
}

2. 规则语言 ​

Starlark 环境主要提供 prefix_rule、network_rule 和 host_executable。prefix_rule 接受 pattern、decision、正反 examples 与 justification;decision 缺省为 Allow。

源码位置:codex-rs/execpolicy/src/parser.rs :: policy_builtins::prefix_rule

rust
fn prefix_rule<'v>(
    pattern: UnpackList<Value<'v>>,
    decision: Option<&'v str>,
    r#match: Option<UnpackList<Value<'v>>>,
    not_match: Option<UnpackList<Value<'v>>>,
    justification: Option<&'v str>,
    eval: &mut Evaluator<'v, '_, '_>,
) -> anyhow::Result<NoneType> {
    let decision = match decision {
        Some(raw) => Decision::parse(raw)?,
        None => Decision::Allow,
    };

    let justification = match justification {
        Some(raw) if raw.trim().is_empty() => {
            return Err(Error::InvalidRule("justification cannot be empty".to_string()).into());
        }
        Some(raw) => Some(raw.to_string()),
        None => None,
    };

    let pattern_tokens = parse_pattern(pattern)?;
    let matches: Vec<Vec<String>> =
        r#match.map(parse_examples).transpose()?.unwrap_or_default();
    let not_matches: Vec<Vec<String>> = not_match
        .map(parse_examples)
        .transpose()?
        .unwrap_or_default();

pattern 为空、decision 非法、justification 空白或 example 无法 token 化都会在加载期失败。example 不是文档装饰:parser.build 会验证声明为 match 的例子至少命中一条本组规则,not_match 则不能命中。

首 token 的 alternatives 会展开成多条规则,便于用 executable 名建立索引;尾部 alternatives 保留为一个 PatternToken::Alts,不会做笛卡尔积爆炸。

源码位置:codex-rs/execpolicy/src/parser.rs :: policy_builtins::prefix_rule

rust
let (first_token, remaining_tokens) = pattern_tokens
    .split_first()
    .ok_or_else(|| Error::InvalidPattern("pattern cannot be empty".to_string()))?;

let rest: Arc<[PatternToken]> = remaining_tokens.to_vec().into();

let rules: Vec<RuleRef> = first_token
    .alternatives()
    .iter()
    .map(|head| {
        Arc::new(PrefixRule {
            pattern: PrefixPattern {
                first: Arc::from(head.as_str()),
                rest: rest.clone(),
            },
            decision,
            justification: justification.clone(),
        }) as RuleRef
    })
    .collect();

builder.add_pending_example_validation(rules.clone(), matches, not_matches, location);
rules.into_iter().for_each(|rule| builder.add_rule(rule));

3. Token匹配 ​

规则匹配命令 token,不匹配未解析 shell 字符串。第一个 token 必须完全相等;后续位置可以是固定 token 或 alternatives;命令可以比 prefix 更长,但不能比它更短。

源码位置:codex-rs/execpolicy/src/rule.rs :: PatternToken, PrefixPattern::matches_prefix

rust
pub enum PatternToken {
    Single(String),
    Alts(Vec<String>),
}

pub fn matches_prefix(&self, cmd: &[String]) -> Option<Vec<String>> {
    let pattern_length = self.rest.len() + 1;
    if cmd.len() < pattern_length || cmd[0] != self.first.as_ref() {
        return None;
    }

    for (pattern_token, cmd_token) in self.rest.iter().zip(&cmd[1..pattern_length]) {
        if !pattern_token.matches(cmd_token) {
            return None;
        }
    }

    Some(cmd[..pattern_length].to_vec())
}

匹配成功返回实际 matched prefix,连同 decision、resolved program 与 justification 写入 RuleMatch::PrefixRuleMatch。Core 后续可以用 justification 生成 prompt/rejection reason,而不需要重新查规则文本。

4. Host可执行文件 ​

命令首 token 可能是绝对路径,例如 /usr/bin/cargo。当 resolve_host_executables=true 时,Policy 可以用 basename 查找规则,但只有 path 在 host_executable(name, paths) 声明的 allowlist 中才接受该 basename rule。

源码位置:codex-rs/execpolicy/src/policy.rs :: Policy::matches_for_command_with_options

rust
let matched_rules = self
    .match_exact_rules(cmd)
    .filter(|matched_rules| !matched_rules.is_empty())
    .or_else(|| {
        options
            .resolve_host_executables
            .then(|| self.match_host_executable_rules(cmd))
            .filter(|matched_rules| !matched_rules.is_empty())
    })
    .unwrap_or_default();

if matched_rules.is_empty()
    && let Some(heuristics_fallback) = heuristics_fallback
{
    vec![RuleMatch::HeuristicsRuleMatch {
        command: cmd.to_vec(),
        decision: heuristics_fallback(cmd),
    }]
} else {
    matched_rules
}

exact path rule 优先于 basename resolution;存在显式空 allowlist 时,任何绝对 path 都不能借 basename rule 匹配。这样避免攻击者把同名恶意 executable 放到任意目录后复用可信规则。

5. Policy合并 ​

Policy 内部包含 prefix multimap、network rules 与 host executable map。overlay 合并会追加 prefix/network rules,但同名 host executable paths 使用 overlay 覆盖。

源码位置:codex-rs/execpolicy/src/policy.rs :: Policy::merge_overlay

rust
let mut combined_rules = self.rules_by_program.clone();
for (program, rules) in overlay.rules_by_program.iter_all() {
    for rule in rules {
        combined_rules.insert(program.clone(), rule.clone());
    }
}

let mut combined_network_rules = self.network_rules.clone();
combined_network_rules.extend(overlay.network_rules.iter().cloned());

let mut host_executables_by_name = self.host_executables_by_name.clone();
host_executables_by_name.extend(
    overlay
        .host_executables_by_name
        .iter()
        .map(|(name, paths)| (name.clone(), paths.clone())),
);

Policy::from_parts(
    combined_rules,
    combined_network_rules,
    host_executables_by_name,
)

prefix 冲突不在 merge 阶段解决,因为严格度由实际匹配集合决定。network domain 编译则按规则顺序 upsert:Allow 会从 denied 删除同 host,Forbidden 会从 allowed 删除,Prompt 不进入静态 allow/deny 列表。

6. Shell命令分段 ​

Core 先尝试解析 shell -lc 内部普通命令;Windows 还会尝试 PowerShell parser。解析成功且结果非空时,每个 segment 单独参与 policy;失败或空脚本则回退原始 argv,不能因 parser 不理解语法而跳过检查。

源码位置:codex-rs/core/src/exec_policy.rs :: commands_for_exec_policy

rust
if let Some(commands) = parse_shell_lc_plain_commands(command)
    && !commands.is_empty()
{
    return ExecPolicyCommands {
        commands,
        command_origin: ExecPolicyCommandOrigin::Generic,
    };
}

#[cfg(windows)]
{
    if let Some(commands) =
        codex_shell_command::powershell::parse_powershell_command_into_plain_commands(command)
        && !commands.is_empty()
    {
        return ExecPolicyCommands {
            commands,
            command_origin: ExecPolicyCommandOrigin::PowerShell,
        };
    }
}

ExecPolicyCommands {
    commands: vec![command.to_vec()],
    command_origin: ExecPolicyCommandOrigin::Generic,
}

heredoc、变量赋值、管道与连接符的处理由 shell parser 决定。复杂结构无法安全缩减成某个允许 prefix 时,Core 保留原始或完整命令 requirement,而不是只批准内部某个看似安全的 segment。

7. 多段聚合 ​

check_multiple_with_options 收集所有 segment 的匹配结果,再交给 Evaluation::from_matches。Decision 的派生顺序是 Allow < Prompt < Forbidden,因此 max() 选择最严格结果。

源码位置:codex-rs/execpolicy/src/decision.rs :: Decision

rust
pub enum Decision {
    Allow,
    Prompt,
    Forbidden,
}

源码位置:codex-rs/execpolicy/src/policy.rs :: Policy::check_multiple_with_options, Evaluation::from_matches

rust
let matched_rules: Vec<RuleMatch> = commands
    .into_iter()
    .flat_map(|command| {
        self.matches_for_command_with_options(
            command.as_ref(),
            Some(heuristics_fallback),
            options,
        )
    })
    .collect();

Evaluation::from_matches(matched_rules)

第一段只完成跨 segment 的 match 收集;最终 decision 在 Evaluation 构造时从完整集合计算。

源码位置:codex-rs/execpolicy/src/policy.rs :: Evaluation::from_matches

rust
let decision = matched_rules.iter().map(RuleMatch::decision).max();
let decision = decision.expect("invariant failed: matched_rules must be non-empty");

Self {
    decision,
    matched_rules,
}

Policy 不采用 first-match,也不让后加载 Allow 覆盖已有 Forbidden。overlay 可以增加限制,但无法通过追加一条宽泛 Allow 消除同一命令命中的更严格规则。

8. 模型Policy视图 ​

ExecPolicyManager 可以为不同模型提供不同 policy 视图。IgnoreForCyberModel 只过滤 Decision::Allow 的 PrefixRule,保留 Prompt、Forbidden、network rules 和 host executable mappings;environment requirements overlay 在过滤之后继续应用。

源码位置:codex-rs/core/src/exec_policy/model_policy.rs :: ExecPolicyManager::current_for_prefix_rules

rust
let policy = self.current();
if allow_prefix_rules == AllowPrefixRules::Honor {
    return policy;
}

let rules = policy
    .rules()
    .iter_all()
    .flat_map(|(program, rules)| {
        rules.iter().filter_map(move |rule| {
            let is_allow_prefix = rule
                .as_any()
                .downcast_ref::<PrefixRule>()
                .is_some_and(|prefix| prefix.decision == Decision::Allow);
            (!is_allow_prefix).then(|| (program.clone(), Arc::clone(rule)))
        })
    })
    .collect();

Arc::new(Policy::from_parts(
    rules,
    policy.network_rules().to_vec(),
    policy.host_executables().clone(),
))

这不是把 policy 全部清空。模型仍然受到明确 Prompt/Forbidden 和 requirements 限制;只是不能利用宽泛 Allow prefix 自动获得可复用放行。

9. Core映射 ​

Core 对所有 segments 调用 policy 后,把最终 Decision 转成 ExecApprovalRequirement。Allow 只有在每个 segment 都显式命中 Allow prefix 时才 bypass_sandbox=true;heuristic Allow 只能得到 Skip 但不绕过 sandbox。

源码位置:codex-rs/core/src/exec_policy.rs :: ExecPolicyManager::create_exec_approval_requirement_for_parsed_commands

rust
let evaluation = exec_policy.check_multiple_with_options(
    commands.iter(),
    &exec_policy_fallback,
    &match_options,
);

match evaluation.decision {
    Decision::Forbidden => ExecApprovalRequirement::Forbidden {
        reason: derive_forbidden_reason(
            command,
            &evaluation,
            dangerous_command_match_for_heuristics(
                &evaluation,
                Decision::Forbidden,
                command_origin,
            ),
        ),
    },
    Decision::Prompt => {
        let prompt_is_rule = evaluation.matched_rules.iter().any(|rule_match| {
            is_policy_match(rule_match) && rule_match.decision() == Decision::Prompt
        });
        match prompt_is_rejected_by_policy(approval_policy, prompt_is_rule) {
            Some(reason) if prompt_is_rule => ExecApprovalRequirement::Forbidden {
                reason: reason.to_string(),
            },
            Some(reason) => ExecApprovalRequirement::Forbidden {
                reason: derive_rejected_prompt_reason(
                    reason,
                    dangerous_command_match_for_heuristics(
                        &evaluation,
                        Decision::Prompt,
                        command_origin,
                    ),
                ),
            },
            None => ExecApprovalRequirement::NeedsApproval {
                reason: derive_prompt_reason(command, &evaluation),
                proposed_execpolicy_amendment: requested_amendment.or_else(|| {
                    if auto_amendment_allowed {
                        try_derive_execpolicy_amendment_for_prompt_rules(
                            &evaluation.matched_rules,
                        )
                    } else {
                        None
                    }
                }),
            },
        }
    }
    Decision::Allow => ExecApprovalRequirement::Skip {
        bypass_sandbox: commands.iter().all(|command| {
            exec_policy
                .matches_for_command_with_options(
                    command,
                    /*heuristics_fallback*/ None,
                    &match_options,
                )
                .iter()
                .any(|rule_match| {
                    is_policy_match(rule_match) && rule_match.decision() == Decision::Allow
                })
        }),
        proposed_execpolicy_amendment: if auto_amendment_allowed {
            try_derive_execpolicy_amendment_for_allow_rules(&evaluation.matched_rules)
        } else {
            None
        },
    },
}

完整 Prompt 分支会区分 rule prompt 与 sandbox prompt 的不同拒绝理由,并在允许 auto amendment 时从显式请求或 heuristic prompt 派生 proposal。下一节展开这部分。

10. Amendment生成 ​

amendment 不是“把整条命令永远允许”。来源有三类:用户显式 prefix_rule 请求、heuristic Prompt 的第一条命令、sandbox 失败后的 heuristic Allow。显式 prefix 必须非空、不在 banned suggestions 中、当前没有 policy rule 匹配,并且临时加入后能让所有解析 segment 都变成 Allow。

源码位置:codex-rs/core/src/exec_policy.rs :: derive_requested_execpolicy_amendment_from_prefix_rule

rust
let prefix_rule = prefix_rule?;
if prefix_rule.is_empty() {
    return None;
}
if BANNED_PREFIX_SUGGESTIONS.iter().any(|banned| {
    prefix_rule.len() == banned.len()
        && prefix_rule
            .iter()
            .map(String::as_str)
            .eq(banned.iter().copied())
}) {
    return None;
}

if matched_rules.iter().any(is_policy_match) {
    return None;
}

let amendment = ExecPolicyAmendment::new(prefix_rule.clone());
if prefix_rule_would_approve_all_commands(
    exec_policy,
    &amendment.command,
    commands,
    exec_policy_fallback,
    match_options,
) {
    Some(amendment)
} else {
    None
}

cyber model 忽略 Allow prefix 时,auto_amendment_allowed=false,防止提示用户创建一个该模型后续也不会信任的可复用 Allow rule。

11. Amendment持久化 ​

用户选择 exec-policy amendment 后,Core 在 blocking 线程中以 advisory lock 追加一行 prefix_rule(..., decision="allow"),然后更新内存 policy。更新锁串行化多个写入;空 prefix、目录创建、锁、读写与序列化错误都有独立类型。

源码位置:codex-rs/execpolicy/src/amend.rs :: blocking_append_allow_prefix_rule

rust
if prefix.is_empty() {
    return Err(AmendError::EmptyPrefix);
}

let tokens = prefix
    .iter()
    .map(serde_json::to_string)
    .collect::<Result<Vec<_>, _>>()
    .map_err(|source| AmendError::SerializePrefix { source })?;
let pattern = format!("[{}]", tokens.join(", "));
let rule = format!(r#"prefix_rule(pattern={pattern}, decision="allow")"#);
append_rule_line(policy_path, &rule)

源码位置:codex-rs/core/src/exec_policy.rs :: ExecPolicyManager::append_amendment_and_update

rust
let _update_guard = self
    .update_lock
    .acquire()
    .await
    .map_err(|_| ExecPolicyUpdateError::AddRule {
        source: ExecPolicyRuleError::InvalidRule(
            "exec policy update semaphore closed".to_string(),
        ),
    })?;
let policy_path = default_policy_path(codex_home);
spawn_blocking({
    let policy_path = policy_path.clone();
    let prefix = amendment.command.clone();
    move || blocking_append_allow_prefix_rule(&policy_path, &prefix)
})
.await
.map_err(|source| ExecPolicyUpdateError::JoinBlockingTask { source })?
.map_err(|source| ExecPolicyUpdateError::AppendRule {
    path: policy_path,
    source,
})?;

文件写入成功后,manager 先检查当前内存 policy 是否已经有显式 Allow;没有时才 clone policy、add rule 并原子替换。磁盘和内存都更新后,下一次命令才能稳定复用该规则。

12. 测试反推边界 ​

ExecPolicy crate 测试覆盖规则语言与聚合,Core 测试覆盖可信层、shell segment、requirements overlay、模型视图和 amendment:

text
cd codex-rs
cargo test -p codex-execpolicy --test basic -- --nocapture --test-threads=1
cargo test -p codex-core --lib exec_policy::tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib exec_policy::model_policy::tests:: -- --nocapture --test-threads=1

三组测试分别通过 27、83、4 项,共 114 项。它们覆盖语言、可信层加载、命令分段、聚合、模型过滤和 amendment;不证明所有 shell 方言都能被 parser 无损拆分。

strictest_decision_wins_across_matches 为同一 command 配置 Prompt 与 Forbidden,断言最终 Forbidden 且两条 match 都保留;strictest_decision_across_multiple_commands 对多个 segment 做同样验证。它们证明 max aggregation,不验证 shell parser 是否拆出了期望 segments。

exec_policies_only_load_from_trusted_project_layers 与无 config trust 配套测试把相同 .rules 放入 trusted/untrusted project,断言只有可信层生效。malformed_custom_rules_preserve_requirements_exec_policy 则让 user rule 解析失败,断言 managed restriction 仍保留。

cyber_policy_filters_allow_prefixes_but_preserves_restrictive_and_network_rules 输入 broad Allow、Prompt、Forbidden、network 与 host executable,断言 cyber view 只删除 Allow prefix。它限定模型 policy 过滤范围,不代表所有模型都使用该模式。

append_execpolicy_amendment_updates_policy_and_file 断言文件追加和内存 manager 同时更新;配套空 prefix 测试断言写入前失败。它验证持久化事务顺序,不验证外部编辑器与 Codex 并发修改时的业务冲突解决。

测试覆盖解析、匹配、聚合、加载和持久化,不能证明任意 shell 语法都可无损拆分。解析失败时 Core 回退原始 argv,是保持检查而非跳过检查的安全边界。

排查时先区分四类问题:规则没有加载看 config trust;规则加载但不命中看 token、absolute executable mapping;单段允许但整体 Prompt/Forbidden 看其他 segment;批准后未来仍询问看 amendment 是否被生成、持久化,以及当前模型是否忽略 Allow prefix。

下一篇Shell命令解析与安全分类将继续深入 shell parser、危险命令识别、heredoc、PowerShell 和 fallback 如何影响 ExecPolicy 输入。