返回 CodeWhale
composition_review.rs
根目录 / crates / tui / src / extension_host / composition_review.rs
1 //! Trusted, nonexecuting DSH preparation. Runs only the embedded reviewer,
2 //! never a package-selected script. Called from the installer's blocking worker.
3 use std::sync::Arc;
4 use std::time::Duration;
5
6 use serde_json::Value;
7 use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
8
9 const REVIEW: &[u8] = include_bytes!("../../extension-host/dist/dsh-composition-review.mjs");
10 const MAX_INPUT: usize = 8 * 1024 * 1024;
11 const MAX_OUTPUT: usize = 8 * 1024 * 1024;
12 const MAX_STDERR: usize = 16 * 1024;
13 const DEADLINE: Duration = Duration::from_secs(5);
14
15 /// Synchronous installer seam; the caller must run outside a Tokio worker.
16 /// No source value or reviewer stderr becomes a public diagnostic.
17 pub(crate) fn review(input: &Value) -> Result<Value, String> {
18 let input = serde_json::to_vec(input).map_err(|_| "invalid composition request")?;
19 if input.len() > MAX_INPUT {
20 return Err("composition review request exceeds 8 MiB".into());
21 }
22 let manager = super::manager();
23 let options = &manager.shared.options;
24 let pin = manager.shared.runtime.lock().expect("runtime lock");
25 let pinned = pin.pinned.clone();
26 let bun_failed = pin.bun_failed;
27 drop(pin);
28 let root = super::host_root(options)?;
29 let review_digest = super::hex(sha2::Sha256::digest(REVIEW));
30 let dir = root
31 .join("extension-host")
32 .join(format!("review-{review_digest}"));
33 let bundle = super::materialize_file(&dir, "dsh-composition-review.mjs", REVIEW)?;
34 super::materialize_file(&dir, super::NOTICES_FILE_NAME, super::NOTICES)?;
35 let runtime = match pinned {
36 Some((runtime, _)) => runtime,
37 None => {
38 let choice = if bun_failed {
39 crate::config::ExtensionHostRuntime::Node
40 } else {
41 options.runtime
42 };
43 crate::dependencies::resolve_extension_host_runtime(
44 choice,
45 options.node_override.as_deref(),
46 options.bun_override.as_deref(),
47 )
48 .selected
49 .ok_or("composition reviewer runtime is unavailable")?
50 }
51 };
52 let launch = super::supervisor::plan_launch(
53 super::tier::HostTier::Plugin,
54 &runtime,
55 &bundle,
56 &root,
57 options.supervision.memory_cap,
58 )?;
59 // A bounded process runner, not another Engine or session runtime.
60 tokio::runtime::Builder::new_current_thread()
61 .enable_all()
62 .build()
63 .map_err(|_| "composition review worker is unavailable")?
64 .block_on(run(launch, input))
65 }
66
67 async fn bounded_read(pipe: impl AsyncRead + Unpin, limit: usize) -> Result<Vec<u8>, String> {
68 let mut bytes = Vec::new();
69 pipe.take((limit + 1) as u64)
70 .read_to_end(&mut bytes)
71 .await
72 .map_err(|_| "composition reviewer pipe failed")?;
73 if bytes.len() > limit {
74 return Err("composition reviewer output exceeded its bound".into());
75 }
76 Ok(bytes)
77 }
78
79 async fn run(launch: super::supervisor::HostLaunch, input: Vec<u8>) -> Result<Value, String> {
80 use std::process::Stdio;
81 let mut command = tokio::process::Command::new(&launch.program);
82 crate::utils::suppress_tokio_console_window(&mut command);
83 command
84 .args(&launch.args)
85 .current_dir(&launch.cwd)
86 .env_clear()
87 .stdin(Stdio::piped())
88 .stdout(Stdio::piped())
89 .stderr(Stdio::piped())
90 .kill_on_drop(true);
91 let overrides = launch
92 .sandbox_env
93 .iter()
94 .chain(&launch.runtime_env)
95 .map(|(key, value)| (key.as_str(), value.as_str()));
96 for (key, value) in
97 crate::child_env::sanitized_plugin_mcp_env_from(std::env::vars_os(), overrides)
98 {
99 command.env(key, value);
100 }
101 #[cfg(unix)]
102 command.process_group(0);
103 super::supervisor::limit_child_memory(&mut command, launch.memory_cap);
104 let mut child = command
105 .spawn()
106 .map_err(|_| "composition reviewer could not start")?;
107 let tree = Arc::new(
108 crate::process_tree::ProcessTree::attach_tokio(&child)
109 .map_err(|_| "composition reviewer containment failed")?,
110 );
111 #[cfg(windows)]
112 tree.limit_process_memory(launch.memory_cap)
113 .map_err(|_| "composition reviewer memory cap failed")?;
114 let pid = child
115 .id()
116 .ok_or("composition reviewer pid is unavailable")?;
117 let mut stdin = child
118 .stdin
119 .take()
120 .ok_or("composition reviewer stdin is unavailable")?;
121 let stdout = child
122 .stdout
123 .take()
124 .ok_or("composition reviewer stdout is unavailable")?;
125 let stderr = child
126 .stderr
127 .take()
128 .ok_or("composition reviewer stderr is unavailable")?;
129 let feed = async move {
130 stdin
131 .write_all(&input)
132 .await
133 .map_err(|_| "composition reviewer input failed".to_string())?;
134 stdin
135 .shutdown()
136 .await
137 .map_err(|_| "composition reviewer input failed".to_string())?;
138 // The reviewer parses a finite document. Closing the pipe is its
139 // end-of-input signal; shutdown alone leaves this handle alive.
140 drop(stdin);
141 Ok::<(), String>(())
142 };
143 let work = async {
144 let (status, output, _stderr, ()) = tokio::try_join!(
145 async {
146 child
147 .wait()
148 .await
149 .map_err(|_| "composition reviewer wait failed".to_string())
150 },
151 bounded_read(stdout, MAX_OUTPUT),
152 bounded_read(stderr, MAX_STDERR),
153 feed,
154 )?;
155 if !status.success() {
156 return Err("composition reviewer refused preparation".into());
157 }
158 serde_json::from_slice(&output)
159 .map_err(|_| "composition reviewer returned invalid JSON".into())
160 };
161 let answer = tokio::select! {
162 answer = tokio::time::timeout(DEADLINE,work) => answer.map_err(|_| "composition reviewer deadline elapsed")?,
163 failure = monitor_memory(pid, launch.memory_cap) => { failure?; unreachable!() },
164 };
165 // Always reap descendants, including any that kept a pipe open; source
166 // modules were never imported. Drop/timeout kills the same guarded tree.
167 let _ = tree.kill();
168 answer
169 }
170
171 use sha2::Digest;
172
173 async fn monitor_memory(pid: u32, cap: u64) -> Result<(), String> {
174 let _ = (pid, cap);
175 loop {
176 tokio::time::sleep(Duration::from_millis(25)).await;
177 #[cfg(target_os = "macos")]
178 if super::supervisor::resident_bytes(pid).is_some_and(|bytes| bytes > cap) {
179 return Err("composition reviewer exceeded its sampled memory cap".into());
180 }
181 }
182 }
183
183 lines RUST