mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Enable --deny-warnings for cargo shear (#21616)
## Summary In https://github.com/openai/codex/pull/21584, we disabled doctests for crates that lack any doctests. We can enforce that property via `cargo shear --deny-warnings`: crates that lack doctests will be flagged if doctests are enabled, and crates with doctests will be flagged if doctests are disabled. A few additional notes: - By adding `--deny-warnings`, `cargo shear` also flagged a number of modules that were not reachable at all. Some of those have been removed. - This PR removes a usage of `windows_modules!` (since `cargo shear` and `rustfmt` couldn't see through it) in favor of simple `#[cfg(target_os = "windows")]` macros. As a consequence, many of these files exhibit churn in this PR, since they weren't being formatted by `rustfmt` at all on main. - Again, to make the code more analyzable, this PR also removes some usages of `#[path = "cwd_junction.rs"]` in favor of a more standard module structure. The bin sidecar structure is still retained, but, e.g., `windows-sandbox-rs/src/bin/command_runner.rs` was moved to `windows-sandbox-rs/src/bin/command_runner/main.rs`, and so on. --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
46e2250bcf
commit
7c9731c9af
@@ -44,10 +44,9 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-shear
|
||||
version: 1.11.2
|
||||
tool: cargo-shear@1.11.2
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
run: cargo shear --deny-warnings
|
||||
|
||||
argument_comment_lint_package:
|
||||
name: Argument comment lint package
|
||||
|
||||
@@ -86,10 +86,9 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-shear
|
||||
version: 1.11.2
|
||||
tool: cargo-shear@1.11.2
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
run: cargo shear --deny-warnings
|
||||
|
||||
argument_comment_lint_package:
|
||||
name: Argument comment lint package
|
||||
|
||||
Generated
-1
@@ -3331,7 +3331,6 @@ dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-image",
|
||||
"codex-utils-string",
|
||||
"codex-utils-template",
|
||||
"encoding_rs",
|
||||
"globset",
|
||||
"http 1.4.0",
|
||||
|
||||
@@ -464,11 +464,8 @@ unwrap_used = "deny"
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = [
|
||||
"codex-agent-graph-store",
|
||||
"codex-memories-mcp",
|
||||
"icu_provider",
|
||||
"openssl-sys",
|
||||
"codex-utils-readiness",
|
||||
"codex-utils-template",
|
||||
"codex-v8-poc",
|
||||
]
|
||||
|
||||
|
||||
@@ -22,6 +22,3 @@ workspace = true
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_with = "3"
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["serde_with"]
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
use super::parse_freeform_args;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_without_pragma() {
|
||||
let args = parse_freeform_args("output_text('ok');").expect("parse args");
|
||||
assert_eq!(args.code, "output_text('ok');");
|
||||
assert_eq!(args.yield_time_ms, None);
|
||||
assert_eq!(args.max_output_tokens, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_with_pragma() {
|
||||
let input = concat!(
|
||||
"// @exec: {\"yield_time_ms\": 15000, \"max_output_tokens\": 2000}\n",
|
||||
"output_text('ok');",
|
||||
);
|
||||
let args = parse_freeform_args(input).expect("parse args");
|
||||
assert_eq!(args.code, "output_text('ok');");
|
||||
assert_eq!(args.yield_time_ms, Some(15_000));
|
||||
assert_eq!(args.max_output_tokens, Some(2_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_rejects_unknown_key() {
|
||||
let err = parse_freeform_args("// @exec: {\"nope\": 1}\noutput_text('ok');")
|
||||
.expect_err("expected error");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"exec pragma only supports `yield_time_ms` and `max_output_tokens`; got `nope`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_rejects_missing_source() {
|
||||
let err = parse_freeform_args("// @exec: {\"yield_time_ms\": 10}").expect_err("expected error");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"exec pragma must be followed by JavaScript source on subsequent lines"
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use super::*;
|
||||
use std::process::Command as StdCommand;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn parses_basic_results() {
|
||||
let stdout = b"/tmp/file_a.rs\n/tmp/file_b.rs\n";
|
||||
let parsed = parse_results(stdout, 10);
|
||||
assert_eq!(
|
||||
parsed,
|
||||
vec!["/tmp/file_a.rs".to_string(), "/tmp/file_b.rs".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_truncates_after_limit() {
|
||||
let stdout = b"/tmp/file_a.rs\n/tmp/file_b.rs\n/tmp/file_c.rs\n";
|
||||
let parsed = parse_results(stdout, 2);
|
||||
assert_eq!(
|
||||
parsed,
|
||||
vec!["/tmp/file_a.rs".to_string(), "/tmp/file_b.rs".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_search_returns_results() -> anyhow::Result<()> {
|
||||
if !rg_available() {
|
||||
return Ok(());
|
||||
}
|
||||
let temp = tempdir().expect("create temp dir");
|
||||
let dir = temp.path();
|
||||
std::fs::write(dir.join("match_one.txt"), "alpha beta gamma").unwrap();
|
||||
std::fs::write(dir.join("match_two.txt"), "alpha delta").unwrap();
|
||||
std::fs::write(dir.join("other.txt"), "omega").unwrap();
|
||||
|
||||
let results = run_rg_search("alpha", None, dir, 10, dir).await?;
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(results.iter().any(|path| path.ends_with("match_one.txt")));
|
||||
assert!(results.iter().any(|path| path.ends_with("match_two.txt")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_search_with_glob_filter() -> anyhow::Result<()> {
|
||||
if !rg_available() {
|
||||
return Ok(());
|
||||
}
|
||||
let temp = tempdir().expect("create temp dir");
|
||||
let dir = temp.path();
|
||||
std::fs::write(dir.join("match_one.rs"), "alpha beta gamma").unwrap();
|
||||
std::fs::write(dir.join("match_two.txt"), "alpha delta").unwrap();
|
||||
|
||||
let results = run_rg_search("alpha", Some("*.rs"), dir, 10, dir).await?;
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results.iter().all(|path| path.ends_with("match_one.rs")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_search_respects_limit() -> anyhow::Result<()> {
|
||||
if !rg_available() {
|
||||
return Ok(());
|
||||
}
|
||||
let temp = tempdir().expect("create temp dir");
|
||||
let dir = temp.path();
|
||||
std::fs::write(dir.join("one.txt"), "alpha one").unwrap();
|
||||
std::fs::write(dir.join("two.txt"), "alpha two").unwrap();
|
||||
std::fs::write(dir.join("three.txt"), "alpha three").unwrap();
|
||||
|
||||
let results = run_rg_search("alpha", None, dir, 2, dir).await?;
|
||||
assert_eq!(results.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_search_handles_no_matches() -> anyhow::Result<()> {
|
||||
if !rg_available() {
|
||||
return Ok(());
|
||||
}
|
||||
let temp = tempdir().expect("create temp dir");
|
||||
let dir = temp.path();
|
||||
std::fs::write(dir.join("one.txt"), "omega").unwrap();
|
||||
|
||||
let results = run_rg_search("alpha", None, dir, 5, dir).await?;
|
||||
assert!(results.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rg_available() -> bool {
|
||||
StdCommand::new("rg")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -1,503 +0,0 @@
|
||||
use super::indentation::read_block;
|
||||
use super::slice::read;
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reads_requested_range() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
temp,
|
||||
"alpha
|
||||
beta
|
||||
gamma
|
||||
"
|
||||
)?;
|
||||
|
||||
let lines = read(temp.path(), 2, 2).await?;
|
||||
assert_eq!(lines, vec!["L2: beta".to_string(), "L3: gamma".to_string()]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn errors_when_offset_exceeds_length() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
writeln!(temp, "only")?;
|
||||
|
||||
let err = read(temp.path(), 3, 1)
|
||||
.await
|
||||
.expect_err("offset exceeds length");
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel("offset exceeds file length".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reads_non_utf8_lines() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
temp.as_file_mut().write_all(b"\xff\xfe\nplain\n")?;
|
||||
|
||||
let lines = read(temp.path(), 1, 2).await?;
|
||||
let expected_first = format!("L1: {}{}", '\u{FFFD}', '\u{FFFD}');
|
||||
assert_eq!(lines, vec![expected_first, "L2: plain".to_string()]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn trims_crlf_endings() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(temp, "one\r\ntwo\r\n")?;
|
||||
|
||||
let lines = read(temp.path(), 1, 2).await?;
|
||||
assert_eq!(lines, vec!["L1: one".to_string(), "L2: two".to_string()]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn respects_limit_even_with_more_lines() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
temp,
|
||||
"first
|
||||
second
|
||||
third
|
||||
"
|
||||
)?;
|
||||
|
||||
let lines = read(temp.path(), 1, 2).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec!["L1: first".to_string(), "L2: second".to_string()]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn truncates_lines_longer_than_max_length() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
let long_line = "x".repeat(MAX_LINE_LENGTH + 50);
|
||||
writeln!(temp, "{long_line}")?;
|
||||
|
||||
let lines = read(temp.path(), 1, 1).await?;
|
||||
let expected = "x".repeat(MAX_LINE_LENGTH);
|
||||
assert_eq!(lines, vec![format!("L1: {expected}")]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn indentation_mode_captures_block() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
temp,
|
||||
"fn outer() {{
|
||||
if cond {{
|
||||
inner();
|
||||
}}
|
||||
tail();
|
||||
}}
|
||||
"
|
||||
)?;
|
||||
|
||||
let options = IndentationArgs {
|
||||
anchor_line: Some(3),
|
||||
include_siblings: false,
|
||||
max_levels: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 3, 10, options).await?;
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L2: if cond {".to_string(),
|
||||
"L3: inner();".to_string(),
|
||||
"L4: }".to_string()
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn indentation_mode_expands_parents() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
temp,
|
||||
"mod root {{
|
||||
fn outer() {{
|
||||
if cond {{
|
||||
inner();
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"
|
||||
)?;
|
||||
|
||||
let mut options = IndentationArgs {
|
||||
anchor_line: Some(4),
|
||||
max_levels: 2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 4, 50, options.clone()).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L2: fn outer() {".to_string(),
|
||||
"L3: if cond {".to_string(),
|
||||
"L4: inner();".to_string(),
|
||||
"L5: }".to_string(),
|
||||
"L6: }".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
options.max_levels = 3;
|
||||
let expanded = read_block(temp.path(), 4, 50, options).await?;
|
||||
assert_eq!(
|
||||
expanded,
|
||||
vec![
|
||||
"L1: mod root {".to_string(),
|
||||
"L2: fn outer() {".to_string(),
|
||||
"L3: if cond {".to_string(),
|
||||
"L4: inner();".to_string(),
|
||||
"L5: }".to_string(),
|
||||
"L6: }".to_string(),
|
||||
"L7: }".to_string(),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn indentation_mode_respects_sibling_flag() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
temp,
|
||||
"fn wrapper() {{
|
||||
if first {{
|
||||
do_first();
|
||||
}}
|
||||
if second {{
|
||||
do_second();
|
||||
}}
|
||||
}}
|
||||
"
|
||||
)?;
|
||||
|
||||
let mut options = IndentationArgs {
|
||||
anchor_line: Some(3),
|
||||
include_siblings: false,
|
||||
max_levels: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 3, 50, options.clone()).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L2: if first {".to_string(),
|
||||
"L3: do_first();".to_string(),
|
||||
"L4: }".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
options.include_siblings = true;
|
||||
let with_siblings = read_block(temp.path(), 3, 50, options).await?;
|
||||
assert_eq!(
|
||||
with_siblings,
|
||||
vec![
|
||||
"L2: if first {".to_string(),
|
||||
"L3: do_first();".to_string(),
|
||||
"L4: }".to_string(),
|
||||
"L5: if second {".to_string(),
|
||||
"L6: do_second();".to_string(),
|
||||
"L7: }".to_string(),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn indentation_mode_handles_python_sample() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
temp,
|
||||
"class Foo:
|
||||
def __init__(self, size):
|
||||
self.size = size
|
||||
def double(self, value):
|
||||
if value is None:
|
||||
return 0
|
||||
result = value * self.size
|
||||
return result
|
||||
class Bar:
|
||||
def compute(self):
|
||||
helper = Foo(2)
|
||||
return helper.double(5)
|
||||
"
|
||||
)?;
|
||||
|
||||
let options = IndentationArgs {
|
||||
anchor_line: Some(7),
|
||||
include_siblings: true,
|
||||
max_levels: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 1, 200, options).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L2: def __init__(self, size):".to_string(),
|
||||
"L3: self.size = size".to_string(),
|
||||
"L4: def double(self, value):".to_string(),
|
||||
"L5: if value is None:".to_string(),
|
||||
"L6: return 0".to_string(),
|
||||
"L7: result = value * self.size".to_string(),
|
||||
"L8: return result".to_string(),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn indentation_mode_handles_javascript_sample() -> anyhow::Result<()> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
temp,
|
||||
"export function makeThing() {{
|
||||
const cache = new Map();
|
||||
function ensure(key) {{
|
||||
if (!cache.has(key)) {{
|
||||
cache.set(key, []);
|
||||
}}
|
||||
return cache.get(key);
|
||||
}}
|
||||
const handlers = {{
|
||||
init() {{
|
||||
console.log(\"init\");
|
||||
}},
|
||||
run() {{
|
||||
if (Math.random() > 0.5) {{
|
||||
return \"heads\";
|
||||
}}
|
||||
return \"tails\";
|
||||
}},
|
||||
}};
|
||||
return {{ cache, handlers }};
|
||||
}}
|
||||
export function other() {{
|
||||
return makeThing();
|
||||
}}
|
||||
"
|
||||
)?;
|
||||
|
||||
let options = IndentationArgs {
|
||||
anchor_line: Some(15),
|
||||
max_levels: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 15, 200, options).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L10: init() {".to_string(),
|
||||
"L11: console.log(\"init\");".to_string(),
|
||||
"L12: },".to_string(),
|
||||
"L13: run() {".to_string(),
|
||||
"L14: if (Math.random() > 0.5) {".to_string(),
|
||||
"L15: return \"heads\";".to_string(),
|
||||
"L16: }".to_string(),
|
||||
"L17: return \"tails\";".to_string(),
|
||||
"L18: },".to_string(),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_cpp_sample() -> anyhow::Result<NamedTempFile> {
|
||||
let mut temp = NamedTempFile::new()?;
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
temp,
|
||||
"#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace sample {{
|
||||
class Runner {{
|
||||
public:
|
||||
void setup() {{
|
||||
if (enabled_) {{
|
||||
init();
|
||||
}}
|
||||
}}
|
||||
|
||||
// Run the code
|
||||
int run() const {{
|
||||
switch (mode_) {{
|
||||
case Mode::Fast:
|
||||
return fast();
|
||||
case Mode::Slow:
|
||||
return slow();
|
||||
default:
|
||||
return fallback();
|
||||
}}
|
||||
}}
|
||||
|
||||
private:
|
||||
bool enabled_ = false;
|
||||
Mode mode_ = Mode::Fast;
|
||||
|
||||
int fast() const {{
|
||||
return 1;
|
||||
}}
|
||||
}};
|
||||
}} // namespace sample
|
||||
"
|
||||
)?;
|
||||
Ok(temp)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn indentation_mode_handles_cpp_sample_shallow() -> anyhow::Result<()> {
|
||||
let temp = write_cpp_sample()?;
|
||||
|
||||
let options = IndentationArgs {
|
||||
include_siblings: false,
|
||||
anchor_line: Some(18),
|
||||
max_levels: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 18, 200, options).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L15: switch (mode_) {".to_string(),
|
||||
"L16: case Mode::Fast:".to_string(),
|
||||
"L17: return fast();".to_string(),
|
||||
"L18: case Mode::Slow:".to_string(),
|
||||
"L19: return slow();".to_string(),
|
||||
"L20: default:".to_string(),
|
||||
"L21: return fallback();".to_string(),
|
||||
"L22: }".to_string(),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn indentation_mode_handles_cpp_sample() -> anyhow::Result<()> {
|
||||
let temp = write_cpp_sample()?;
|
||||
|
||||
let options = IndentationArgs {
|
||||
include_siblings: false,
|
||||
anchor_line: Some(18),
|
||||
max_levels: 2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 18, 200, options).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L13: // Run the code".to_string(),
|
||||
"L14: int run() const {".to_string(),
|
||||
"L15: switch (mode_) {".to_string(),
|
||||
"L16: case Mode::Fast:".to_string(),
|
||||
"L17: return fast();".to_string(),
|
||||
"L18: case Mode::Slow:".to_string(),
|
||||
"L19: return slow();".to_string(),
|
||||
"L20: default:".to_string(),
|
||||
"L21: return fallback();".to_string(),
|
||||
"L22: }".to_string(),
|
||||
"L23: }".to_string(),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn indentation_mode_handles_cpp_sample_no_headers() -> anyhow::Result<()> {
|
||||
let temp = write_cpp_sample()?;
|
||||
|
||||
let options = IndentationArgs {
|
||||
include_siblings: false,
|
||||
include_header: false,
|
||||
anchor_line: Some(18),
|
||||
max_levels: 2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 18, 200, options).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L14: int run() const {".to_string(),
|
||||
"L15: switch (mode_) {".to_string(),
|
||||
"L16: case Mode::Fast:".to_string(),
|
||||
"L17: return fast();".to_string(),
|
||||
"L18: case Mode::Slow:".to_string(),
|
||||
"L19: return slow();".to_string(),
|
||||
"L20: default:".to_string(),
|
||||
"L21: return fallback();".to_string(),
|
||||
"L22: }".to_string(),
|
||||
"L23: }".to_string(),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn indentation_mode_handles_cpp_sample_siblings() -> anyhow::Result<()> {
|
||||
let temp = write_cpp_sample()?;
|
||||
|
||||
let options = IndentationArgs {
|
||||
include_siblings: true,
|
||||
include_header: false,
|
||||
anchor_line: Some(18),
|
||||
max_levels: 2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lines = read_block(temp.path(), 18, 200, options).await?;
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"L7: void setup() {".to_string(),
|
||||
"L8: if (enabled_) {".to_string(),
|
||||
"L9: init();".to_string(),
|
||||
"L10: }".to_string(),
|
||||
"L11: }".to_string(),
|
||||
"L12: ".to_string(),
|
||||
"L13: // Run the code".to_string(),
|
||||
"L14: int run() const {".to_string(),
|
||||
"L15: switch (mode_) {".to_string(),
|
||||
"L16: case Mode::Fast:".to_string(),
|
||||
"L17: return fast();".to_string(),
|
||||
"L18: case Mode::Slow:".to_string(),
|
||||
"L19: return slow();".to_string(),
|
||||
"L20: default:".to_string(),
|
||||
"L21: return fallback();".to_string(),
|
||||
"L22: }".to_string(),
|
||||
"L23: }".to_string(),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -65,6 +65,7 @@ mod models_cache_ttl;
|
||||
mod models_etag_responses;
|
||||
mod openai_file_mcp;
|
||||
mod otel;
|
||||
mod override_updates;
|
||||
mod pending_input;
|
||||
mod permissions_messages;
|
||||
mod personality;
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
use anyhow::Result;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::COLLABORATION_MODE_CLOSE_TAG;
|
||||
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMode {
|
||||
@@ -33,77 +23,9 @@ fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMod
|
||||
}
|
||||
}
|
||||
|
||||
fn collab_xml(text: &str) -> String {
|
||||
format!("{COLLABORATION_MODE_OPEN_TAG}{text}{COLLABORATION_MODE_CLOSE_TAG}")
|
||||
}
|
||||
|
||||
async fn read_rollout_text(path: &Path) -> anyhow::Result<String> {
|
||||
for _ in 0..50 {
|
||||
if path.exists()
|
||||
&& let Ok(text) = std::fs::read_to_string(path)
|
||||
&& !text.trim().is_empty()
|
||||
{
|
||||
return Ok(text);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
Ok(std::fs::read_to_string(path)?)
|
||||
}
|
||||
|
||||
fn rollout_developer_texts(text: &str) -> Vec<String> {
|
||||
let mut texts = Vec::new();
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rollout: RolloutLine = match serde_json::from_str(trimmed) {
|
||||
Ok(rollout) => rollout,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) =
|
||||
rollout.item
|
||||
&& role == "developer"
|
||||
{
|
||||
for item in content {
|
||||
if let ContentItem::InputText { text } = item {
|
||||
texts.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
texts
|
||||
}
|
||||
|
||||
fn rollout_environment_texts(text: &str) -> Vec<String> {
|
||||
let mut texts = Vec::new();
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rollout: RolloutLine = match serde_json::from_str(trimmed) {
|
||||
Ok(rollout) => rollout,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) =
|
||||
rollout.item
|
||||
&& role == "user"
|
||||
{
|
||||
for item in content {
|
||||
if let ContentItem::InputText { text } = item
|
||||
&& text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG)
|
||||
{
|
||||
texts.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
texts
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn override_turn_context_without_user_turn_does_not_record_permissions_update() -> Result<()> {
|
||||
async fn override_turn_context_without_user_turn_does_not_record_permissions_update() -> Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -133,22 +55,17 @@ async fn override_turn_context_without_user_turn_does_not_record_permissions_upd
|
||||
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;
|
||||
|
||||
let rollout_path = test.codex.rollout_path().expect("rollout path");
|
||||
let rollout_text = read_rollout_text(&rollout_path).await?;
|
||||
let developer_texts = rollout_developer_texts(&rollout_text);
|
||||
let approval_texts: Vec<&String> = developer_texts
|
||||
.iter()
|
||||
.filter(|text| text.contains("`approval_policy`"))
|
||||
.collect();
|
||||
assert!(
|
||||
approval_texts.is_empty(),
|
||||
"did not expect permissions updates before a new user turn: {approval_texts:?}"
|
||||
!rollout_path.exists(),
|
||||
"did not expect a rollout before a new user turn"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn override_turn_context_without_user_turn_does_not_record_environment_update() -> Result<()> {
|
||||
async fn override_turn_context_without_user_turn_does_not_record_environment_update() -> Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -176,18 +93,17 @@ async fn override_turn_context_without_user_turn_does_not_record_environment_upd
|
||||
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;
|
||||
|
||||
let rollout_path = test.codex.rollout_path().expect("rollout path");
|
||||
let rollout_text = read_rollout_text(&rollout_path).await?;
|
||||
let env_texts = rollout_environment_texts(&rollout_text);
|
||||
assert!(
|
||||
env_texts.is_empty(),
|
||||
"did not expect environment updates before a new user turn: {env_texts:?}"
|
||||
!rollout_path.exists(),
|
||||
"did not expect a rollout before a new user turn"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn override_turn_context_without_user_turn_does_not_record_collaboration_update() -> Result<()> {
|
||||
async fn override_turn_context_without_user_turn_does_not_record_collaboration_update() -> Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -216,14 +132,10 @@ async fn override_turn_context_without_user_turn_does_not_record_collaboration_u
|
||||
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;
|
||||
|
||||
let rollout_path = test.codex.rollout_path().expect("rollout path");
|
||||
let rollout_text = read_rollout_text(&rollout_path).await?;
|
||||
let developer_texts = rollout_developer_texts(&rollout_text);
|
||||
let collab_text = collab_xml(collab_text);
|
||||
let collab_count = developer_texts
|
||||
.iter()
|
||||
.filter(|text| text.as_str() == collab_text.as_str())
|
||||
.count();
|
||||
assert_eq!(collab_count, 0);
|
||||
assert!(
|
||||
!rollout_path.exists(),
|
||||
"did not expect a rollout before a new user turn"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use serde_json::Value;
|
||||
|
||||
pub(crate) fn invalid_request(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: -32600,
|
||||
data: None,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_params(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: -32602,
|
||||
data: None,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn method_not_found(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: -32601,
|
||||
data: None,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn response_message(
|
||||
request_id: RequestId,
|
||||
result: Result<Value, JSONRPCErrorError>,
|
||||
) -> JSONRPCMessage {
|
||||
match result {
|
||||
Ok(result) => JSONRPCMessage::Response(JSONRPCResponse {
|
||||
id: request_id,
|
||||
result,
|
||||
}),
|
||||
Err(error) => JSONRPCMessage::Error(JSONRPCError {
|
||||
id: request_id,
|
||||
error,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_request_message(reason: String) -> JSONRPCMessage {
|
||||
JSONRPCMessage::Error(JSONRPCError {
|
||||
id: RequestId::Integer(-1),
|
||||
error: invalid_request(reason),
|
||||
})
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Hook;
|
||||
use crate::HookEvent;
|
||||
use crate::HookPayload;
|
||||
use crate::HookResult;
|
||||
use crate::command_from_argv;
|
||||
|
||||
/// Legacy notify payload appended as the final argv argument for backward compatibility.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
enum UserNotification {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
AgentTurnComplete {
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
cwd: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client: Option<String>,
|
||||
|
||||
/// Messages that the user sent to the agent to initiate the turn.
|
||||
input_messages: Vec<String>,
|
||||
|
||||
/// The last message sent by the assistant in the turn.
|
||||
last_assistant_message: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn legacy_notify_json(payload: &HookPayload) -> Result<String, serde_json::Error> {
|
||||
match &payload.hook_event {
|
||||
HookEvent::AfterAgent { event } => {
|
||||
serde_json::to_string(&UserNotification::AgentTurnComplete {
|
||||
thread_id: event.thread_id.to_string(),
|
||||
turn_id: event.turn_id.clone(),
|
||||
cwd: payload.cwd.display().to_string(),
|
||||
client: payload.client.clone(),
|
||||
input_messages: event.input_messages.clone(),
|
||||
last_assistant_message: event.last_assistant_message.clone(),
|
||||
})
|
||||
}
|
||||
_ => Err(serde_json::Error::io(std::io::Error::other(
|
||||
"legacy notify payload is only supported for after_agent",
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_hook(argv: Vec<String>) -> Hook {
|
||||
let argv = Arc::new(argv);
|
||||
Hook {
|
||||
name: "legacy_notify".to_string(),
|
||||
func: Arc::new(move |payload: &HookPayload| {
|
||||
let argv = Arc::clone(&argv);
|
||||
Box::pin(async move {
|
||||
let mut command = match command_from_argv(&argv) {
|
||||
Some(command) => command,
|
||||
None => return HookResult::Success,
|
||||
};
|
||||
if let Ok(notify_payload) = legacy_notify_json(payload) {
|
||||
command.arg(notify_payload);
|
||||
}
|
||||
|
||||
// Backwards-compat: match legacy notify behavior (argv + JSON arg, fire-and-forget).
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
match command.spawn() {
|
||||
Ok(_) => HookResult::Success,
|
||||
Err(err) => HookResult::FailedContinue(err.into()),
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn expected_notification_json() -> Value {
|
||||
let cwd = test_path_buf("/Users/example/project");
|
||||
json!({
|
||||
"type": "agent-turn-complete",
|
||||
"thread-id": "b5f6c1c2-1111-2222-3333-444455556666",
|
||||
"turn-id": "12345",
|
||||
"cwd": cwd.display().to_string(),
|
||||
"client": "codex-tui",
|
||||
"input-messages": ["Rename `foo` to `bar` and update the callsites."],
|
||||
"last-assistant-message": "Rename complete and verified `cargo build` succeeds.",
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_notification() -> Result<()> {
|
||||
let notification = UserNotification::AgentTurnComplete {
|
||||
thread_id: "b5f6c1c2-1111-2222-3333-444455556666".to_string(),
|
||||
turn_id: "12345".to_string(),
|
||||
cwd: test_path_buf("/Users/example/project")
|
||||
.display()
|
||||
.to_string(),
|
||||
client: Some("codex-tui".to_string()),
|
||||
input_messages: vec!["Rename `foo` to `bar` and update the callsites.".to_string()],
|
||||
last_assistant_message: Some(
|
||||
"Rename complete and verified `cargo build` succeeds.".to_string(),
|
||||
),
|
||||
};
|
||||
let serialized = serde_json::to_string(¬ification)?;
|
||||
let actual: Value = serde_json::from_str(&serialized)?;
|
||||
assert_eq!(actual, expected_notification_json());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_notify_json_matches_historical_wire_shape() -> Result<()> {
|
||||
let payload = HookPayload {
|
||||
session_id: ThreadId::new(),
|
||||
cwd: test_path_buf("/Users/example/project").abs(),
|
||||
client: Some("codex-tui".to_string()),
|
||||
triggered_at: chrono::Utc::now(),
|
||||
hook_event: HookEvent::AfterAgent {
|
||||
event: crate::HookEventAfterAgent {
|
||||
thread_id: ThreadId::from_string("b5f6c1c2-1111-2222-3333-444455556666")
|
||||
.expect("valid thread id"),
|
||||
turn_id: "12345".to_string(),
|
||||
input_messages: vec![
|
||||
"Rename `foo` to `bar` and update the callsites.".to_string(),
|
||||
],
|
||||
last_assistant_message: Some(
|
||||
"Rename complete and verified `cargo build` succeeds.".to_string(),
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let serialized = legacy_notify_json(&payload)?;
|
||||
let actual: Value = serde_json::from_str(&serialized)?;
|
||||
assert_eq!(actual, expected_notification_json());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub(crate) mod create_conversation;
|
||||
pub(crate) mod send_message;
|
||||
@@ -1,70 +0,0 @@
|
||||
//! Resolve plugin namespace from skill file paths by walking ancestors for `plugin.json`.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Relative path from a plugin root to its manifest file.
|
||||
pub const PLUGIN_MANIFEST_PATH: &str = ".codex-plugin/plugin.json";
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RawPluginManifestName {
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn plugin_manifest_name(plugin_root: &Path) -> Option<String> {
|
||||
let manifest_path = plugin_root.join(PLUGIN_MANIFEST_PATH);
|
||||
if !manifest_path.is_file() {
|
||||
return None;
|
||||
}
|
||||
let contents = fs::read_to_string(&manifest_path).ok()?;
|
||||
let RawPluginManifestName { name: raw_name } = serde_json::from_str(&contents).ok()?;
|
||||
Some(
|
||||
plugin_root
|
||||
.file_name()
|
||||
.and_then(|entry| entry.to_str())
|
||||
.filter(|_| raw_name.trim().is_empty())
|
||||
.unwrap_or(raw_name.as_str())
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the plugin manifest `name` for the nearest ancestor of `path` that contains a valid
|
||||
/// plugin manifest (same `name` rules as full manifest loading in codex-core).
|
||||
pub fn plugin_namespace_for_skill_path(path: &Path) -> Option<String> {
|
||||
for ancestor in path.ancestors() {
|
||||
if let Some(name) = plugin_manifest_name(ancestor) {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::plugin_namespace_for_skill_path;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn uses_manifest_name() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let plugin_root = tmp.path().join("plugins/sample");
|
||||
let skill_path = plugin_root.join("skills/search/SKILL.md");
|
||||
|
||||
fs::create_dir_all(skill_path.parent().expect("parent")).expect("mkdir");
|
||||
fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("mkdir manifest");
|
||||
fs::write(
|
||||
plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"sample"}"#,
|
||||
)
|
||||
.expect("write manifest");
|
||||
fs::write(&skill_path, "---\ndescription: search\n---\n").expect("write skill");
|
||||
|
||||
assert_eq!(
|
||||
plugin_namespace_for_skill_path(&skill_path),
|
||||
Some("sample".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ codex-network-proxy = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-image = { workspace = true }
|
||||
codex-utils-string = { workspace = true }
|
||||
codex-utils-template = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
icu_decimal = { workspace = true }
|
||||
@@ -60,5 +59,4 @@ tempfile = { workspace = true }
|
||||
[package.metadata.cargo-shear]
|
||||
# Required because:
|
||||
# `icu_provider`: contains a required `sync` feature for `icu_decimal`
|
||||
# `strum`: as strum_macros in non-nightly builds
|
||||
ignored = ["icu_provider", "strum"]
|
||||
ignored = ["icu_provider"]
|
||||
|
||||
@@ -12,11 +12,11 @@ doctest = false
|
||||
|
||||
[[bin]]
|
||||
name = "codex-windows-sandbox-setup"
|
||||
path = "src/bin/setup_main.rs"
|
||||
path = "src/bin/setup_main/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "codex-command-runner"
|
||||
path = "src/bin/command_runner.rs"
|
||||
path = "src/bin/command_runner/main.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -96,6 +96,3 @@ pretty_assertions = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["codex-utils-pty", "tokio"]
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE;
|
||||
use windows_sys::Win32::Security::ACE_HEADER;
|
||||
use windows_sys::Win32::Security::ACL;
|
||||
use windows_sys::Win32::Security::ACL_SIZE_INFORMATION;
|
||||
use windows_sys::Win32::Security::AclSizeInformation;
|
||||
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
||||
use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW;
|
||||
use windows_sys::Win32::Security::Authorization::GetSecurityInfo;
|
||||
use windows_sys::Win32::Security::Authorization::SetEntriesInAclW;
|
||||
use windows_sys::Win32::Security::Authorization::SetNamedSecurityInfoW;
|
||||
use windows_sys::Win32::Security::Authorization::SetSecurityInfo;
|
||||
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
||||
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||
use windows_sys::Win32::Security::EqualSid;
|
||||
use windows_sys::Win32::Security::GENERIC_MAPPING;
|
||||
use windows_sys::Win32::Security::GetAce;
|
||||
use windows_sys::Win32::Security::GetAclInformation;
|
||||
use windows_sys::Win32::Security::MapGenericMask;
|
||||
use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE;
|
||||
use windows_sys::Win32::Security::ACE_HEADER;
|
||||
use windows_sys::Win32::Security::ACL;
|
||||
use windows_sys::Win32::Security::ACL_SIZE_INFORMATION;
|
||||
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||
use windows_sys::Win32::Security::GENERIC_MAPPING;
|
||||
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
|
||||
use windows_sys::Win32::Storage::FileSystem::DELETE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ALL_ACCESS;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_APPEND_DATA;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_DELETE_CHILD;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
|
||||
@@ -45,7 +46,6 @@ use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_DATA;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA;
|
||||
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
|
||||
use windows_sys::Win32::Storage::FileSystem::READ_CONTROL;
|
||||
use windows_sys::Win32::Storage::FileSystem::DELETE;
|
||||
const SE_KERNEL_OBJECT: u32 = 6;
|
||||
const INHERIT_ONLY_ACE: u8 = 0x08;
|
||||
const GENERIC_WRITE_MASK: u32 = 0x4000_0000;
|
||||
@@ -259,12 +259,8 @@ pub unsafe fn dacl_has_write_deny_for_sid(p_dacl: *mut ACL, psid: *mut c_void) -
|
||||
false
|
||||
}
|
||||
|
||||
const WRITE_ALLOW_MASK: u32 = FILE_GENERIC_READ
|
||||
| FILE_GENERIC_WRITE
|
||||
| FILE_GENERIC_EXECUTE
|
||||
| DELETE
|
||||
| FILE_DELETE_CHILD;
|
||||
|
||||
const WRITE_ALLOW_MASK: u32 =
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE | FILE_DELETE_CHILD;
|
||||
|
||||
unsafe fn ensure_allow_mask_aces_with_inheritance_impl(
|
||||
path: &Path,
|
||||
@@ -275,12 +271,7 @@ unsafe fn ensure_allow_mask_aces_with_inheritance_impl(
|
||||
let (p_dacl, p_sd) = fetch_dacl_handle(path)?;
|
||||
let mut entries: Vec<EXPLICIT_ACCESS_W> = Vec::new();
|
||||
for sid in sids {
|
||||
if dacl_mask_allows(
|
||||
p_dacl,
|
||||
&[*sid],
|
||||
allow_mask,
|
||||
/*require_all_bits*/ true,
|
||||
) {
|
||||
if dacl_mask_allows(p_dacl, &[*sid], allow_mask, /*require_all_bits*/ true) {
|
||||
continue;
|
||||
}
|
||||
entries.push(EXPLICIT_ACCESS_W {
|
||||
|
||||
@@ -117,12 +117,16 @@ mod tests {
|
||||
|
||||
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
|
||||
|
||||
assert!(paths
|
||||
.allow
|
||||
.contains(&dunce::canonicalize(&command_cwd).unwrap()));
|
||||
assert!(paths
|
||||
.allow
|
||||
.contains(&dunce::canonicalize(&extra_root).unwrap()));
|
||||
assert!(
|
||||
paths
|
||||
.allow
|
||||
.contains(&dunce::canonicalize(&command_cwd).unwrap())
|
||||
);
|
||||
assert!(
|
||||
paths
|
||||
.allow
|
||||
.contains(&dunce::canonicalize(&extra_root).unwrap())
|
||||
);
|
||||
assert!(paths.deny.is_empty(), "no deny paths expected");
|
||||
}
|
||||
|
||||
@@ -145,12 +149,16 @@ mod tests {
|
||||
|
||||
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map);
|
||||
|
||||
assert!(paths
|
||||
.allow
|
||||
.contains(&dunce::canonicalize(&command_cwd).unwrap()));
|
||||
assert!(!paths
|
||||
.allow
|
||||
.contains(&dunce::canonicalize(&temp_dir).unwrap()));
|
||||
assert!(
|
||||
paths
|
||||
.allow
|
||||
.contains(&dunce::canonicalize(&command_cwd).unwrap())
|
||||
);
|
||||
assert!(
|
||||
!paths
|
||||
.allow
|
||||
.contains(&dunce::canonicalize(&temp_dir).unwrap())
|
||||
);
|
||||
assert!(paths.deny.is_empty(), "no deny paths expected");
|
||||
}
|
||||
|
||||
@@ -253,6 +261,9 @@ mod tests {
|
||||
|
||||
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
|
||||
assert_eq!(paths.allow.len(), 1);
|
||||
assert!(paths.deny.is_empty(), "no deny when protected dirs are absent");
|
||||
assert!(
|
||||
paths.deny.is_empty(),
|
||||
"no deny when protected dirs are absent"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,17 @@ use crate::acl::add_deny_write_ace;
|
||||
use crate::acl::path_mask_allows;
|
||||
use crate::cap::cap_sid_file;
|
||||
use crate::cap::load_or_create_cap_sids;
|
||||
use crate::logging::{debug_log, log_note};
|
||||
use crate::logging::debug_log;
|
||||
use crate::logging::log_note;
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::token::convert_string_sid_to_sid;
|
||||
use crate::token::world_sid;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::c_void;
|
||||
use std::ffi::OsStr;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -81,7 +82,12 @@ unsafe fn path_has_world_write_allow(path: &Path) -> Result<bool> {
|
||||
let mut world = world_sid()?;
|
||||
let psid_world = world.as_mut_ptr() as *mut c_void;
|
||||
let write_mask = FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES;
|
||||
path_mask_allows(path, &[psid_world], write_mask, /*require_all_bits*/ false)
|
||||
path_mask_allows(
|
||||
path,
|
||||
&[psid_world],
|
||||
write_mask,
|
||||
/*require_all_bits*/ false,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn audit_everyone_writable(
|
||||
@@ -262,9 +268,8 @@ pub fn apply_capability_denies_for_world_writable(
|
||||
(sid, roots)
|
||||
}
|
||||
SandboxPolicy::ReadOnly { .. } => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly) }.ok_or_else(|| {
|
||||
anyhow!("ConvertStringSidToSidW failed for readonly capability")
|
||||
})?,
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly) }
|
||||
.ok_or_else(|| anyhow!("ConvertStringSidToSidW failed for readonly capability"))?,
|
||||
Vec::new(),
|
||||
),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#[path = "../elevated/command_runner_win.rs"]
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
+24
-9
@@ -7,9 +7,10 @@
|
||||
//! accepts stdin/terminate frames, and emits a final exit frame. The legacy restricted‑token
|
||||
//! path spawns the child directly and does not use this runner.
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
#![allow(unsafe_op_in_unsafe_fn)]
|
||||
|
||||
mod cwd_junction;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_windows_sandbox::ErrorPayload;
|
||||
@@ -40,6 +41,7 @@ use codex_windows_sandbox::read_handle_loop;
|
||||
use codex_windows_sandbox::spawn_process_with_pipes;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::write_frame;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::File;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::path::Path;
|
||||
@@ -48,6 +50,7 @@ use std::ptr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
@@ -66,17 +69,13 @@ use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
|
||||
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||
use windows_sys::Win32::System::Threading::GetProcessId;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
use windows_sys::Win32::System::Threading::MUTEX_ALL_ACCESS;
|
||||
use windows_sys::Win32::System::Threading::OpenMutexW;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||
use windows_sys::Win32::System::Threading::TerminateProcess;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
|
||||
#[path = "cwd_junction.rs"]
|
||||
mod cwd_junction;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[path = "../read_acl_mutex.rs"]
|
||||
mod read_acl_mutex;
|
||||
|
||||
const READ_ACL_MUTEX_NAME: &str = "Local\\CodexSandboxReadAcl";
|
||||
const WAIT_TIMEOUT: u32 = 0x0000_0102;
|
||||
|
||||
struct IpcSpawnedProcess {
|
||||
@@ -196,9 +195,25 @@ fn read_spawn_request(reader: &mut File) -> Result<SpawnRequest> {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_acl_mutex_exists() -> Result<bool> {
|
||||
let name = to_wide(OsStr::new(READ_ACL_MUTEX_NAME));
|
||||
let handle = unsafe { OpenMutexW(MUTEX_ALL_ACCESS, 0, name.as_ptr()) };
|
||||
if handle == 0 {
|
||||
let err = unsafe { GetLastError() };
|
||||
if err == ERROR_FILE_NOT_FOUND {
|
||||
return Ok(false);
|
||||
}
|
||||
return Err(anyhow::anyhow!("OpenMutexW failed: {err}"));
|
||||
}
|
||||
unsafe {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Pick an effective CWD, using a junction if the ACL helper is active.
|
||||
fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
|
||||
let use_junction = match read_acl_mutex::read_acl_mutex_exists() {
|
||||
let use_junction = match read_acl_mutex_exists() {
|
||||
Ok(exists) => exists,
|
||||
Err(err) => {
|
||||
log_note(
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use codex_windows_sandbox::log_note;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hash;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#[path = "../setup_main_win.rs"]
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
+1
-4
@@ -1,6 +1,5 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
mod firewall;
|
||||
mod read_acl_mutex;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
@@ -67,9 +66,7 @@ use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
|
||||
|
||||
const DENY_ACCESS: i32 = 3;
|
||||
|
||||
mod read_acl_mutex;
|
||||
mod sandbox_users;
|
||||
#[path = "setup_runtime_bin.rs"]
|
||||
mod setup_runtime_bin;
|
||||
use read_acl_mutex::acquire_read_acl_mutex;
|
||||
use read_acl_mutex::read_acl_mutex_exists;
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use anyhow::Result;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
+4
-5
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use std::ffi::OsStr;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::ERROR_ALREADY_EXISTS;
|
||||
@@ -10,11 +11,9 @@ use windows_sys::Win32::System::Threading::MUTEX_ALL_ACCESS;
|
||||
use windows_sys::Win32::System::Threading::OpenMutexW;
|
||||
use windows_sys::Win32::System::Threading::ReleaseMutex;
|
||||
|
||||
use super::to_wide;
|
||||
|
||||
const READ_ACL_MUTEX_NAME: &str = "Local\\CodexSandboxReadAcl";
|
||||
|
||||
pub struct ReadAclMutexGuard {
|
||||
pub(super) struct ReadAclMutexGuard {
|
||||
handle: HANDLE,
|
||||
}
|
||||
|
||||
@@ -27,7 +26,7 @@ impl Drop for ReadAclMutexGuard {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_acl_mutex_exists() -> Result<bool> {
|
||||
pub(super) fn read_acl_mutex_exists() -> Result<bool> {
|
||||
let name = to_wide(OsStr::new(READ_ACL_MUTEX_NAME));
|
||||
let handle = unsafe { OpenMutexW(MUTEX_ALL_ACCESS, 0, name.as_ptr()) };
|
||||
if handle == 0 {
|
||||
@@ -43,7 +42,7 @@ pub fn read_acl_mutex_exists() -> Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn acquire_read_acl_mutex() -> Result<Option<ReadAclMutexGuard>> {
|
||||
pub(super) fn acquire_read_acl_mutex() -> Result<Option<ReadAclMutexGuard>> {
|
||||
let name = to_wide(OsStr::new(READ_ACL_MUTEX_NAME));
|
||||
let handle = unsafe { CreateMutexW(std::ptr::null_mut(), 1, name.as_ptr()) };
|
||||
if handle == 0 {
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::RngCore;
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::SmallRng;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct CapSids {
|
||||
@@ -106,7 +106,12 @@ mod tests {
|
||||
std::fs::create_dir_all(&workspace).expect("create workspace root");
|
||||
|
||||
let canonical = dunce::canonicalize(&workspace).expect("canonical workspace root");
|
||||
let alt_spelling = PathBuf::from(canonical.to_string_lossy().replace('\\', "/").to_ascii_uppercase());
|
||||
let alt_spelling = PathBuf::from(
|
||||
canonical
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
.to_ascii_uppercase(),
|
||||
);
|
||||
|
||||
let first_sid =
|
||||
workspace_cap_sid_for_cwd(&codex_home, canonical.as_path()).expect("first sid");
|
||||
|
||||
@@ -7,13 +7,13 @@ use anyhow::Result;
|
||||
use rand::Rng;
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::SmallRng;
|
||||
use std::path::Path;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
||||
use windows_sys::Win32::Security::Authorization::GRANT_ACCESS;
|
||||
@@ -25,16 +25,16 @@ use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
||||
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::CloseDesktop;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_CREATEMENU;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::CreateDesktopW;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_CREATEMENU;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_CREATEWINDOW;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_DELETE;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_ENUMERATE;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_HOOKCONTROL;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_JOURNALPLAYBACK;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_JOURNALRECORD;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_READOBJECTS;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_READ_CONTROL;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_READOBJECTS;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_SWITCHDESKTOP;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_WRITE_DAC;
|
||||
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_WRITE_OWNER;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Security::Cryptography::CryptProtectData;
|
||||
use windows_sys::Win32::Security::Cryptography::CryptUnprotectData;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Security::Cryptography::CRYPT_INTEGER_BLOB;
|
||||
use windows_sys::Win32::Security::Cryptography::CRYPTPROTECT_LOCAL_MACHINE;
|
||||
use windows_sys::Win32::Security::Cryptography::CRYPTPROTECT_UI_FORBIDDEN;
|
||||
use windows_sys::Win32::Security::Cryptography::CRYPT_INTEGER_BLOB;
|
||||
use windows_sys::Win32::Security::Cryptography::CryptProtectData;
|
||||
use windows_sys::Win32::Security::Cryptography::CryptUnprotectData;
|
||||
|
||||
fn make_blob(data: &[u8]) -> CRYPT_INTEGER_BLOB {
|
||||
CRYPT_INTEGER_BLOB {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) mod ipc_framed;
|
||||
pub(crate) mod runner_client;
|
||||
pub(crate) mod runner_pipe;
|
||||
@@ -1,10 +1,13 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use dirs_next::home_dir;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::fs::File;
|
||||
use std::fs::{self};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn normalize_null_device_env(env_map: &mut HashMap<String, String>) {
|
||||
let keys: Vec<String> = env_map.keys().cloned().collect();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
@@ -89,10 +89,7 @@ pub(crate) fn resolve_helper_for_launch(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_current_exe_for_launch(
|
||||
codex_home: &Path,
|
||||
fallback_executable: &str,
|
||||
) -> PathBuf {
|
||||
pub fn resolve_current_exe_for_launch(codex_home: &Path, fallback_executable: &str) -> PathBuf {
|
||||
let source = match std::env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return PathBuf::from(fallback_executable),
|
||||
@@ -242,11 +239,7 @@ fn dev_build_suffix(source: &Path) -> Result<String> {
|
||||
let duration = modified
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.with_context(|| format!("convert helper source mtime {}", source.display()))?;
|
||||
Ok(format!(
|
||||
"{}-{:x}",
|
||||
metadata.len(),
|
||||
duration.as_secs(),
|
||||
))
|
||||
Ok(format!("{}-{:x}", metadata.len(), duration.as_secs(),))
|
||||
}
|
||||
|
||||
fn copy_from_source_if_needed(source: &Path, destination: &Path) -> Result<CopyOutcome> {
|
||||
@@ -254,9 +247,12 @@ fn copy_from_source_if_needed(source: &Path, destination: &Path) -> Result<CopyO
|
||||
return Ok(CopyOutcome::Reused);
|
||||
}
|
||||
|
||||
let destination_dir = destination
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("helper destination has no parent: {}", destination.display()))?;
|
||||
let destination_dir = destination.parent().ok_or_else(|| {
|
||||
anyhow!(
|
||||
"helper destination has no parent: {}",
|
||||
destination.display()
|
||||
)
|
||||
})?;
|
||||
fs::create_dir_all(destination_dir).with_context(|| {
|
||||
format!(
|
||||
"create helper destination directory {}",
|
||||
@@ -327,8 +323,9 @@ fn destination_is_fresh(source: &Path, destination: &Path) -> Result<bool> {
|
||||
Ok(meta) => meta,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
||||
Err(err) => {
|
||||
return Err(err)
|
||||
.with_context(|| format!("read helper destination metadata {}", destination.display()));
|
||||
return Err(err).with_context(|| {
|
||||
format!("read helper destination metadata {}", destination.display())
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -348,16 +345,16 @@ fn destination_is_fresh(source: &Path, destination: &Path) -> Result<bool> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::copy_from_source_if_needed;
|
||||
use super::CopyOutcome;
|
||||
use super::dev_build_suffix;
|
||||
use super::DEV_BUILD_VERSION_SENTINEL;
|
||||
use super::HelperExecutable;
|
||||
use super::RESOURCES_DIRNAME;
|
||||
use super::copy_from_source_if_needed;
|
||||
use super::destination_is_fresh;
|
||||
use super::dev_build_suffix;
|
||||
use super::helper_bin_dir;
|
||||
use super::helper_version_suffix;
|
||||
use super::materialized_file_name;
|
||||
use super::HelperExecutable;
|
||||
use super::DEV_BUILD_VERSION_SENTINEL;
|
||||
use super::RESOURCES_DIRNAME;
|
||||
use super::source_path_for_exe;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
@@ -376,7 +373,10 @@ mod tests {
|
||||
let outcome = copy_from_source_if_needed(&source, &destination).expect("copy helper");
|
||||
|
||||
assert_eq!(CopyOutcome::ReCopied, outcome);
|
||||
assert_eq!(b"runner-v1".as_slice(), fs::read(&destination).expect("read destination"));
|
||||
assert_eq!(
|
||||
b"runner-v1".as_slice(),
|
||||
fs::read(&destination).expect("read destination")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -403,11 +403,13 @@ mod tests {
|
||||
fs::write(&source, b"runner-v1").expect("write source");
|
||||
copy_from_source_if_needed(&source, &destination).expect("initial copy");
|
||||
|
||||
let outcome =
|
||||
copy_from_source_if_needed(&source, &destination).expect("revalidate helper");
|
||||
let outcome = copy_from_source_if_needed(&source, &destination).expect("revalidate helper");
|
||||
|
||||
assert_eq!(CopyOutcome::Reused, outcome);
|
||||
assert_eq!(b"runner-v1".as_slice(), fs::read(&destination).expect("read destination"));
|
||||
assert_eq!(
|
||||
b"runner-v1".as_slice(),
|
||||
fs::read(&destination).expect("read destination")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -429,8 +431,10 @@ mod tests {
|
||||
let runner_source = source_dir.join("codex-command-runner.exe");
|
||||
fs::write(&runner_source, b"runner").expect("runner");
|
||||
let runner_suffix = helper_version_suffix(&runner_source).expect("runner suffix");
|
||||
let runner_destination = helper_bin_dir(&codex_home)
|
||||
.join(materialized_file_name(HelperExecutable::CommandRunner, &runner_suffix));
|
||||
let runner_destination = helper_bin_dir(&codex_home).join(materialized_file_name(
|
||||
HelperExecutable::CommandRunner,
|
||||
&runner_suffix,
|
||||
));
|
||||
|
||||
let runner_outcome =
|
||||
copy_from_source_if_needed(&runner_source, &runner_destination).expect("runner copy");
|
||||
@@ -453,8 +457,8 @@ mod tests {
|
||||
fs::write(&exe, b"codex").expect("write exe");
|
||||
fs::write(&helper, b"runner").expect("write helper");
|
||||
|
||||
let resolved =
|
||||
source_path_for_exe(&exe, /*file_name*/ "codex-command-runner.exe").expect("helper path");
|
||||
let resolved = source_path_for_exe(&exe, /*file_name*/ "codex-command-runner.exe")
|
||||
.expect("helper path");
|
||||
|
||||
assert_eq!(resolved, helper);
|
||||
}
|
||||
@@ -472,8 +476,8 @@ mod tests {
|
||||
fs::write(&sibling_helper, b"sibling runner").expect("write sibling helper");
|
||||
fs::write(&resource_helper, b"resource runner").expect("write resource helper");
|
||||
|
||||
let resolved =
|
||||
source_path_for_exe(&exe, /*file_name*/ "codex-command-runner.exe").expect("helper path");
|
||||
let resolved = source_path_for_exe(&exe, /*file_name*/ "codex-command-runner.exe")
|
||||
.expect("helper path");
|
||||
|
||||
assert_eq!(resolved, sibling_helper);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use crate::logging::log_note;
|
||||
use crate::winutil::format_last_error;
|
||||
use crate::winutil::to_wide;
|
||||
@@ -8,19 +6,19 @@ use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Storage::FileSystem::GetFileAttributesW;
|
||||
use windows_sys::Win32::Storage::FileSystem::SetFileAttributesW;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_HIDDEN;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SYSTEM;
|
||||
use windows_sys::Win32::Storage::FileSystem::GetFileAttributesW;
|
||||
use windows_sys::Win32::Storage::FileSystem::INVALID_FILE_ATTRIBUTES;
|
||||
use windows_sys::Win32::System::Registry::RegCloseKey;
|
||||
use windows_sys::Win32::System::Registry::RegCreateKeyExW;
|
||||
use windows_sys::Win32::System::Registry::RegSetValueExW;
|
||||
use windows_sys::Win32::Storage::FileSystem::SetFileAttributesW;
|
||||
use windows_sys::Win32::System::Registry::HKEY;
|
||||
use windows_sys::Win32::System::Registry::HKEY_LOCAL_MACHINE;
|
||||
use windows_sys::Win32::System::Registry::KEY_WRITE;
|
||||
use windows_sys::Win32::System::Registry::REG_DWORD;
|
||||
use windows_sys::Win32::System::Registry::REG_OPTION_NON_VOLATILE;
|
||||
use windows_sys::Win32::System::Registry::RegCloseKey;
|
||||
use windows_sys::Win32::System::Registry::RegCreateKeyExW;
|
||||
use windows_sys::Win32::System::Registry::RegSetValueExW;
|
||||
|
||||
const USERLIST_KEY_PATH: &str =
|
||||
r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList";
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use crate::dpapi;
|
||||
use crate::logging::debug_log;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::setup::SandboxNetworkIdentity;
|
||||
use crate::setup::SandboxUserRecord;
|
||||
use crate::setup::SandboxUsersFile;
|
||||
use crate::setup::SetupMarker;
|
||||
use crate::setup::gather_read_roots;
|
||||
use crate::setup::gather_write_roots;
|
||||
use crate::setup::offline_proxy_settings_from_env;
|
||||
@@ -8,15 +12,11 @@ use crate::setup::run_elevated_setup;
|
||||
use crate::setup::run_setup_refresh_with_overrides;
|
||||
use crate::setup::sandbox_users_path;
|
||||
use crate::setup::setup_marker_path;
|
||||
use crate::setup::SandboxNetworkIdentity;
|
||||
use crate::setup::SandboxUserRecord;
|
||||
use crate::setup::SandboxUsersFile;
|
||||
use crate::setup::SetupMarker;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use anyhow::anyhow;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
@@ -150,8 +150,7 @@ pub fn require_logon_sandbox_creds(
|
||||
.map(<[PathBuf]>::to_vec)
|
||||
.unwrap_or_else(|| gather_write_roots(policy, policy_cwd, command_cwd, env_map));
|
||||
let network_identity = SandboxNetworkIdentity::from_policy(policy, proxy_enforced);
|
||||
let desired_offline_proxy_settings =
|
||||
offline_proxy_settings_from_env(env_map, network_identity);
|
||||
let desired_offline_proxy_settings = offline_proxy_settings_from_env(env_map, network_identity);
|
||||
// NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the
|
||||
// restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for
|
||||
// granting the sandbox group access to this directory without granting the capability SID.
|
||||
@@ -167,8 +166,9 @@ pub fn require_logon_sandbox_creds(
|
||||
} else {
|
||||
let selected = select_identity(network_identity, codex_home)?;
|
||||
if selected.is_none() {
|
||||
setup_reason =
|
||||
Some("sandbox users missing or incompatible with marker version".to_string());
|
||||
setup_reason = Some(
|
||||
"sandbox users missing or incompatible with marker version".to_string(),
|
||||
);
|
||||
}
|
||||
selected
|
||||
}
|
||||
|
||||
@@ -5,75 +5,70 @@
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
mod ssh_config_dependencies;
|
||||
|
||||
macro_rules! windows_modules {
|
||||
($($name:ident),+ $(,)?) => {
|
||||
$(#[cfg(target_os = "windows")] mod $name;)+
|
||||
};
|
||||
}
|
||||
|
||||
windows_modules!(
|
||||
acl,
|
||||
allow,
|
||||
audit,
|
||||
cap,
|
||||
desktop,
|
||||
dpapi,
|
||||
env,
|
||||
helper_materialization,
|
||||
hide_users,
|
||||
identity,
|
||||
logging,
|
||||
path_normalization,
|
||||
policy,
|
||||
process,
|
||||
token,
|
||||
wfp,
|
||||
wfp_setup,
|
||||
winutil,
|
||||
workspace_acl
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
mod acl;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod allow;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod audit;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod cap;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod desktop;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod dpapi;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod elevated;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod elevated_impl;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod env;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod helper_materialization;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod hide_users;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod identity;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod logging;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod path_normalization;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod policy;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod proc_thread_attr;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod process;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod sandbox_utils;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod setup;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod setup_error;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod spawn_prep;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod token;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod unified_exec;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod wfp;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod wfp_setup;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod winutil;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod workspace_acl;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "conpty/mod.rs"]
|
||||
mod conpty;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "proc_thread_attr.rs"]
|
||||
mod proc_thread_attr;
|
||||
|
||||
pub(crate) use elevated::ipc_framed;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "elevated/ipc_framed.rs"]
|
||||
pub(crate) mod ipc_framed;
|
||||
|
||||
pub(crate) use elevated::runner_client;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "setup_orchestrator.rs"]
|
||||
mod setup;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod elevated_impl;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "elevated/runner_pipe.rs"]
|
||||
mod runner_pipe;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "elevated/runner_client.rs"]
|
||||
mod runner_client;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod setup_error;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "sandbox_utils.rs"]
|
||||
mod sandbox_utils;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "spawn_prep.rs"]
|
||||
mod spawn_prep;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "unified_exec/session.rs"]
|
||||
mod session;
|
||||
pub(crate) use elevated::runner_pipe;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::add_deny_write_ace;
|
||||
@@ -169,10 +164,6 @@ pub use process::read_handle_loop;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use process::spawn_process_with_pipes;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use session::spawn_windows_sandbox_session_elevated;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use session::spawn_windows_sandbox_session_legacy;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::SETUP_VERSION;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::SandboxSetupRequest;
|
||||
@@ -222,6 +213,10 @@ pub use token::create_workspace_write_token_with_caps_from;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::get_current_token_for_restriction;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use unified_exec::spawn_windows_sandbox_session_elevated;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use unified_exec::spawn_windows_sandbox_session_legacy;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use wfp::install_wfp_filters_for_account;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use wfp_setup::install_wfp_filters;
|
||||
|
||||
@@ -23,6 +23,9 @@ mod tests {
|
||||
let windows_style = Path::new(r"C:\Users\Dev\Repo");
|
||||
let slash_style = Path::new("c:/users/dev/repo");
|
||||
|
||||
assert_eq!(canonical_path_key(windows_style), canonical_path_key(slash_style));
|
||||
assert_eq!(
|
||||
canonical_path_key(windows_style),
|
||||
canonical_path_key(slash_style)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ pub fn parse_policy(value: &str) -> Result<SandboxPolicy> {
|
||||
match value {
|
||||
"read-only" => Ok(SandboxPolicy::new_read_only_policy()),
|
||||
"workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()),
|
||||
"danger-full-access" | "external-sandbox" => anyhow::bail!(
|
||||
"DangerFullAccess and ExternalSandbox are not supported for sandboxing"
|
||||
),
|
||||
"danger-full-access" | "external-sandbox" => {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
other => {
|
||||
let parsed: SandboxPolicy = serde_json::from_str(other)?;
|
||||
if matches!(
|
||||
@@ -31,23 +31,24 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_external_sandbox_preset() {
|
||||
let err = parse_policy("external-sandbox").unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("DangerFullAccess and ExternalSandbox are not supported"));
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("DangerFullAccess and ExternalSandbox are not supported")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_external_sandbox_json() {
|
||||
let payload = serde_json::to_string(
|
||||
&codex_protocol::protocol::SandboxPolicy::ExternalSandbox {
|
||||
let payload =
|
||||
serde_json::to_string(&codex_protocol::protocol::SandboxPolicy::ExternalSandbox {
|
||||
network_access: codex_protocol::protocol::NetworkAccess::Enabled,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
let err = parse_policy(&payload).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("DangerFullAccess and ExternalSandbox are not supported"));
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("DangerFullAccess and ExternalSandbox are not supported")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,26 +4,26 @@ use crate::proc_thread_attr::ProcThreadAttributeList;
|
||||
use crate::winutil::argv_to_command_line;
|
||||
use crate::winutil::format_last_error;
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::SetHandleInformation;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::Foundation::SetHandleInformation;
|
||||
use windows_sys::Win32::Storage::FileSystem::ReadFile;
|
||||
use windows_sys::Win32::System::Console::GetStdHandle;
|
||||
use windows_sys::Win32::System::Console::STD_ERROR_HANDLE;
|
||||
use windows_sys::Win32::System::Console::STD_INPUT_HANDLE;
|
||||
use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
|
||||
use windows_sys::Win32::System::Pipes::CreatePipe;
|
||||
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
|
||||
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
|
||||
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
|
||||
use windows_sys::Win32::System::Threading::EXTENDED_STARTUPINFO_PRESENT;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use std::ffi::c_void;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::LUID;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Security::AdjustTokenPrivileges;
|
||||
use windows_sys::Win32::Security::Authorization::SetEntriesInAclW;
|
||||
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
||||
use windows_sys::Win32::Security::Authorization::GRANT_ACCESS;
|
||||
use windows_sys::Win32::Security::Authorization::SetEntriesInAclW;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
||||
@@ -24,9 +24,6 @@ use windows_sys::Win32::Security::GetTokenInformation;
|
||||
use windows_sys::Win32::Security::LookupPrivilegeValueW;
|
||||
use windows_sys::Win32::Security::SetTokenInformation;
|
||||
|
||||
use windows_sys::Win32::Security::TokenDefaultDacl;
|
||||
use windows_sys::Win32::Security::TokenGroups;
|
||||
use windows_sys::Win32::Security::TokenUser;
|
||||
use windows_sys::Win32::Security::ACL;
|
||||
use windows_sys::Win32::Security::SID_AND_ATTRIBUTES;
|
||||
use windows_sys::Win32::Security::TOKEN_ADJUST_DEFAULT;
|
||||
@@ -37,6 +34,9 @@ use windows_sys::Win32::Security::TOKEN_DUPLICATE;
|
||||
use windows_sys::Win32::Security::TOKEN_PRIVILEGES;
|
||||
use windows_sys::Win32::Security::TOKEN_QUERY;
|
||||
use windows_sys::Win32::Security::TOKEN_USER;
|
||||
use windows_sys::Win32::Security::TokenDefaultDacl;
|
||||
use windows_sys::Win32::Security::TokenGroups;
|
||||
use windows_sys::Win32::Security::TokenUser;
|
||||
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
||||
|
||||
const DISABLE_MAX_PRIVILEGE: u32 = 0x01;
|
||||
@@ -136,11 +136,7 @@ pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> {
|
||||
}
|
||||
let mut psid: *mut c_void = std::ptr::null_mut();
|
||||
let ok = unsafe { ConvertStringSidToSidW(to_wide(s).as_ptr(), &mut psid) };
|
||||
if ok != 0 {
|
||||
Some(psid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if ok != 0 { Some(psid) } else { None }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
@@ -276,7 +272,10 @@ unsafe fn get_user_sid_bytes(h_token: HANDLE) -> Result<Vec<u8>> {
|
||||
let token_user: TOKEN_USER = std::ptr::read_unaligned(user_buf.as_ptr() as *const TOKEN_USER);
|
||||
let sid_len = GetLengthSid(token_user.User.Sid);
|
||||
if sid_len == 0 {
|
||||
return Err(anyhow!("GetLengthSid(TokenUser) failed: {}", GetLastError()));
|
||||
return Err(anyhow!(
|
||||
"GetLengthSid(TokenUser) failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
let mut user_sid_bytes = vec![0u8; sid_len as usize];
|
||||
if CopySid(
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
#[path = "wfp_filter_specs.rs"]
|
||||
mod filter_specs;
|
||||
|
||||
use crate::to_wide;
|
||||
@@ -9,23 +6,22 @@ use std::ffi::OsStr;
|
||||
use std::mem::zeroed;
|
||||
use std::ptr::null;
|
||||
use std::ptr::null_mut;
|
||||
use windows_sys::core::GUID;
|
||||
use windows_sys::Win32::Foundation::FWP_E_ALREADY_EXISTS;
|
||||
use windows_sys::Win32::Foundation::FWP_E_FILTER_NOT_FOUND;
|
||||
use windows_sys::Win32::Foundation::FWP_E_NOT_FOUND;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_ACTRL_MATCH_FILTER;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_ACTION_BLOCK;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_ACTRL_MATCH_FILTER;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_BYTE_BLOB;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_EMPTY;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_MATCH_EQUAL;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_SECURITY_DESCRIPTOR_TYPE;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_UINT16;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_UINT8;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_UINT16;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_VALUE0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_ACTION0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_ACTION0_0;
|
||||
@@ -33,15 +29,15 @@ use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_CONDIT
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_CONDITION_IP_PROTOCOL;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_CONDITION_IP_REMOTE_PORT;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_DISPLAY_DATA0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER0_0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER_CONDITION0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER_FLAG_PERSISTENT;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_PROVIDER0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_FILTER0_0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_PROVIDER_FLAG_PERSISTENT;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_PROVIDER0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_SESSION0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_SUBLAYER0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_SUBLAYER_FLAG_PERSISTENT;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_SUBLAYER0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmEngineClose0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmEngineOpen0;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmFilterAdd0;
|
||||
@@ -58,6 +54,7 @@ use windows_sys::Win32::Security::Authorization::GRANT_ACCESS;
|
||||
use windows_sys::Win32::Security::PSECURITY_DESCRIPTOR;
|
||||
use windows_sys::Win32::System::Rpc::RPC_C_AUTHN_DEFAULT;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
use windows_sys::core::GUID;
|
||||
|
||||
use filter_specs::ConditionSpec;
|
||||
use filter_specs::FILTER_SPECS;
|
||||
@@ -267,7 +264,11 @@ fn ensure_sublayer(engine: HANDLE) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Adds one blocking WFP filter from the static filter spec list.
|
||||
fn add_filter(engine: HANDLE, spec: &FilterSpec, user_condition: &UserMatchCondition) -> Result<()> {
|
||||
fn add_filter(
|
||||
engine: HANDLE,
|
||||
spec: &FilterSpec,
|
||||
user_condition: &UserMatchCondition,
|
||||
) -> Result<()> {
|
||||
let filter_name = to_wide(OsStr::new(spec.name));
|
||||
let filter_description = to_wide(OsStr::new(spec.description));
|
||||
let mut filter_conditions = build_conditions(spec.conditions, user_condition);
|
||||
@@ -288,7 +289,9 @@ fn add_filter(engine: HANDLE, spec: &FilterSpec, user_condition: &UserMatchCondi
|
||||
filterCondition: filter_conditions.as_mut_ptr(),
|
||||
action: FWPM_ACTION0 {
|
||||
r#type: FWP_ACTION_BLOCK,
|
||||
Anonymous: FWPM_ACTION0_0 { filterType: zero_guid() },
|
||||
Anonymous: FWPM_ACTION0_0 {
|
||||
filterType: zero_guid(),
|
||||
},
|
||||
},
|
||||
Anonymous: FWPM_FILTER0_0 { rawContext: 0 },
|
||||
reserved: null_mut(),
|
||||
@@ -396,14 +399,24 @@ mod tests {
|
||||
fn filter_keys_are_unique() {
|
||||
let keys = FILTER_SPECS
|
||||
.iter()
|
||||
.map(|spec| (spec.key.data1, spec.key.data2, spec.key.data3, spec.key.data4))
|
||||
.map(|spec| {
|
||||
(
|
||||
spec.key.data1,
|
||||
spec.key.data2,
|
||||
spec.key.data3,
|
||||
spec.key.data4,
|
||||
)
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(keys.len(), FILTER_SPECS.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_names_are_unique() {
|
||||
let names = FILTER_SPECS.iter().map(|spec| spec.name).collect::<BTreeSet<_>>();
|
||||
let names = FILTER_SPECS
|
||||
.iter()
|
||||
.map(|spec| spec.name)
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(names.len(), FILTER_SPECS.len());
|
||||
}
|
||||
}
|
||||
|
||||
+17
-7
@@ -1,12 +1,10 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use windows_sys::core::GUID;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_LAYER_ALE_AUTH_CONNECT_V4;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_LAYER_ALE_AUTH_CONNECT_V6;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4;
|
||||
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V6;
|
||||
use windows_sys::Win32::Networking::WinSock::IPPROTO_ICMP;
|
||||
use windows_sys::Win32::Networking::WinSock::IPPROTO_ICMPV6;
|
||||
use windows_sys::core::GUID;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) enum ConditionSpec {
|
||||
@@ -30,28 +28,40 @@ pub(super) const FILTER_SPECS: &[FilterSpec] = &[
|
||||
name: "codex_wfp_icmp_connect_v4",
|
||||
description: "Block sandbox-account ICMP connect v4",
|
||||
layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V4,
|
||||
conditions: &[ConditionSpec::User, ConditionSpec::Protocol(IPPROTO_ICMP as u8)],
|
||||
conditions: &[
|
||||
ConditionSpec::User,
|
||||
ConditionSpec::Protocol(IPPROTO_ICMP as u8),
|
||||
],
|
||||
},
|
||||
FilterSpec {
|
||||
key: GUID::from_u128(0x87498484_45ab_4510_845e_ece8b791b3bc),
|
||||
name: "codex_wfp_icmp_connect_v6",
|
||||
description: "Block sandbox-account ICMP connect v6",
|
||||
layer_key: FWPM_LAYER_ALE_AUTH_CONNECT_V6,
|
||||
conditions: &[ConditionSpec::User, ConditionSpec::Protocol(IPPROTO_ICMPV6 as u8)],
|
||||
conditions: &[
|
||||
ConditionSpec::User,
|
||||
ConditionSpec::Protocol(IPPROTO_ICMPV6 as u8),
|
||||
],
|
||||
},
|
||||
FilterSpec {
|
||||
key: GUID::from_u128(0xaf4751de_f874_4a7b_a34d_f0d0f22d1d9b),
|
||||
name: "codex_wfp_icmp_assign_v4",
|
||||
description: "Block sandbox-account ICMP resource assignment v4",
|
||||
layer_key: FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4,
|
||||
conditions: &[ConditionSpec::User, ConditionSpec::Protocol(IPPROTO_ICMP as u8)],
|
||||
conditions: &[
|
||||
ConditionSpec::User,
|
||||
ConditionSpec::Protocol(IPPROTO_ICMP as u8),
|
||||
],
|
||||
},
|
||||
FilterSpec {
|
||||
key: GUID::from_u128(0xea10db66_a928_4b2e_a82e_a376a54f93ba),
|
||||
name: "codex_wfp_icmp_assign_v6",
|
||||
description: "Block sandbox-account ICMP resource assignment v6",
|
||||
layer_key: FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V6,
|
||||
conditions: &[ConditionSpec::User, ConditionSpec::Protocol(IPPROTO_ICMPV6 as u8)],
|
||||
conditions: &[
|
||||
ConditionSpec::User,
|
||||
ConditionSpec::Protocol(IPPROTO_ICMPV6 as u8),
|
||||
],
|
||||
},
|
||||
// NAME_RESOLUTION_CACHE filters are intentionally omitted because ordinary
|
||||
// static filter shapes returned FWP_E_OUT_OF_BOUNDS during validation.
|
||||
@@ -171,11 +171,5 @@ pub fn install_wfp_filters<F>(
|
||||
}
|
||||
};
|
||||
|
||||
emit_wfp_setup_metric_safely(
|
||||
codex_home,
|
||||
otel,
|
||||
offline_username,
|
||||
&metric,
|
||||
&mut log,
|
||||
);
|
||||
emit_wfp_setup_metric_safely(codex_home, otel, offline_username, &metric, &mut log);
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@ use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
|
||||
use windows_sys::Win32::Security::Authorization::ConvertStringSidToSidW;
|
||||
use windows_sys::Win32::Security::CopySid;
|
||||
use windows_sys::Win32::Security::GetLengthSid;
|
||||
use windows_sys::Win32::Security::LookupAccountNameW;
|
||||
use windows_sys::Win32::Security::SID_NAME_USE;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FormatMessageW;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_FROM_SYSTEM;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FormatMessageW;
|
||||
|
||||
pub fn to_wide<S: AsRef<OsStr>>(s: S) -> Vec<u16> {
|
||||
let mut v: Vec<u16> = s.as_ref().encode_wide().collect();
|
||||
@@ -107,7 +107,10 @@ pub fn string_from_sid_bytes(sid: &[u8]) -> Result<String, String> {
|
||||
let mut str_ptr: *mut u16 = std::ptr::null_mut();
|
||||
let ok = ConvertSidToStringSidW(sid.as_ptr() as *mut std::ffi::c_void, &mut str_ptr);
|
||||
if ok == 0 || str_ptr.is_null() {
|
||||
return Err(format!("ConvertSidToStringSidW failed: {}", std::io::Error::last_os_error()));
|
||||
return Err(format!(
|
||||
"ConvertSidToStringSidW failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
let mut len = 0;
|
||||
while *str_ptr.add(len) != 0 {
|
||||
@@ -158,7 +161,9 @@ pub fn resolve_sid(name: &str) -> Result<Vec<u8>> {
|
||||
domain.resize(domain_len as usize, 0);
|
||||
continue;
|
||||
}
|
||||
return Err(anyhow::anyhow!("LookupAccountNameW failed for {name}: {err}"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"LookupAccountNameW failed for {name}: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user