Skip to content

Commit 4e7d513

Browse files
Merge pull request #422 from compoundingtech/schickling-assistant/2026-09-02-github-resource-contract-alignment
feat(resources): align built-in provider components
2 parents 3fd2802 + 66e7751 commit 4e7d513

24 files changed

Lines changed: 4562 additions & 1966 deletions

File tree

components/github-issue/src/lib.rs

Lines changed: 372 additions & 66 deletions
Large diffs are not rendered by default.

components/github-pr/src/lib.rs

Lines changed: 397 additions & 198 deletions
Large diffs are not rendered by default.

components/pty-stats/src/lib.rs

Lines changed: 471 additions & 74 deletions
Large diffs are not rendered by default.

components/vista/src/lib.rs

Lines changed: 320 additions & 65 deletions
Large diffs are not rendered by default.
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
use std::io::{self, Read as _};
2+
use std::os::fd::AsRawFd as _;
3+
use std::os::unix::process::CommandExt as _;
4+
use std::path::Path;
5+
use std::process::{Child, ChildStdout, Command, ExitStatus, Stdio};
6+
use std::time::{Duration, Instant};
7+
8+
use reqwest::header::HeaderValue;
9+
10+
const MAX_TOKEN_BYTES: usize = 4096;
11+
12+
pub(crate) fn discover_authorization(executable: &Path, deadline: Instant) -> Option<HeaderValue> {
13+
if Instant::now() >= deadline {
14+
return None;
15+
}
16+
let mut command = Command::new(executable);
17+
command
18+
.args(["auth", "token", "--hostname", "github.com"])
19+
.env("GH_PROMPT_DISABLED", "1")
20+
.env("GIT_TERMINAL_PROMPT", "0")
21+
.stdin(Stdio::null())
22+
.stdout(Stdio::piped())
23+
.stderr(Stdio::null())
24+
.process_group(0);
25+
let child = command.spawn().ok()?;
26+
let mut child = OwnedChild::new(child).ok()?;
27+
let mut stdout = child.child.stdout.take()?;
28+
let (status, token) = collect_token(&mut child, &mut stdout, deadline)?;
29+
if !status.success() {
30+
return None;
31+
}
32+
authorization_header(token)
33+
}
34+
35+
fn collect_token(
36+
child: &mut OwnedChild,
37+
stdout: &mut ChildStdout,
38+
deadline: Instant,
39+
) -> Option<(ExitStatus, SecretBytes)> {
40+
let mut token = SecretBytes(Vec::with_capacity(128));
41+
let mut status = None;
42+
let mut stdout_closed = false;
43+
let mut buffer = SecretBuffer([0_u8; 1024]);
44+
45+
loop {
46+
if Instant::now() >= deadline {
47+
child.terminate_and_reap();
48+
return None;
49+
}
50+
if status.is_none() {
51+
status = child.try_wait().ok()?;
52+
if status.is_some() {
53+
// The direct child is reaped by try_wait. Terminate any descendants that retained
54+
// stdout so the dedicated process group cannot outlive credential discovery.
55+
child.terminate_group();
56+
}
57+
}
58+
if stdout_closed {
59+
if let Some(status) = status {
60+
return Some((status, token));
61+
}
62+
std::thread::sleep(
63+
deadline
64+
.saturating_duration_since(Instant::now())
65+
.min(Duration::from_millis(1)),
66+
);
67+
continue;
68+
}
69+
70+
let remaining = deadline.saturating_duration_since(Instant::now());
71+
let timeout_ms = i32::try_from(remaining.as_millis()).unwrap_or(i32::MAX);
72+
let mut descriptor = libc::pollfd {
73+
fd: stdout.as_raw_fd(),
74+
events: libc::POLLIN | libc::POLLHUP | libc::POLLERR,
75+
revents: 0,
76+
};
77+
// SAFETY: descriptor points to one valid pollfd for the duration of this call.
78+
let polled = unsafe { libc::poll(&mut descriptor, 1, timeout_ms) };
79+
if polled < 0 {
80+
if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
81+
continue;
82+
}
83+
return None;
84+
}
85+
if polled == 0 {
86+
continue;
87+
}
88+
if descriptor.revents & libc::POLLNVAL != 0 {
89+
return None;
90+
}
91+
if descriptor.revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) == 0 {
92+
continue;
93+
}
94+
95+
let read = if token.0.len() == MAX_TOKEN_BYTES {
96+
let mut overflow = SecretBuffer([0_u8; 1]);
97+
match stdout.read(&mut overflow.0) {
98+
Ok(0) => {
99+
stdout_closed = true;
100+
continue;
101+
}
102+
Ok(_) => {
103+
child.terminate_and_reap();
104+
return None;
105+
}
106+
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
107+
Err(_) => return None,
108+
}
109+
} else {
110+
let available = (MAX_TOKEN_BYTES - token.0.len()).min(buffer.0.len());
111+
match stdout.read(&mut buffer.0[..available]) {
112+
Ok(0) => {
113+
stdout_closed = true;
114+
continue;
115+
}
116+
Ok(read) => read,
117+
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
118+
Err(_) => return None,
119+
}
120+
};
121+
token.0.extend_from_slice(&buffer.0[..read]);
122+
}
123+
}
124+
125+
fn authorization_header(token: SecretBytes) -> Option<HeaderValue> {
126+
let token_text = std::str::from_utf8(&token.0).ok()?;
127+
let token_text = token_text.trim();
128+
if token_text.is_empty() {
129+
return None;
130+
}
131+
let mut header = SecretBytes(Vec::with_capacity("Bearer ".len() + token_text.len()));
132+
header.0.extend_from_slice(b"Bearer ");
133+
header.0.extend_from_slice(token_text.as_bytes());
134+
let mut value = HeaderValue::from_bytes(&header.0).ok()?;
135+
value.set_sensitive(true);
136+
Some(value)
137+
}
138+
139+
struct SecretBytes(Vec<u8>);
140+
141+
impl Drop for SecretBytes {
142+
fn drop(&mut self) {
143+
self.0.fill(0);
144+
}
145+
}
146+
147+
struct SecretBuffer<const N: usize>([u8; N]);
148+
149+
impl<const N: usize> Drop for SecretBuffer<N> {
150+
fn drop(&mut self) {
151+
self.0.fill(0);
152+
}
153+
}
154+
155+
struct OwnedChild {
156+
child: Child,
157+
process_group: i32,
158+
reaped: bool,
159+
}
160+
161+
impl OwnedChild {
162+
fn new(mut child: Child) -> io::Result<Self> {
163+
let process_group = match i32::try_from(child.id()) {
164+
Ok(process_group) => process_group,
165+
Err(_) => {
166+
let _ = child.kill();
167+
let _ = child.wait();
168+
return Err(io::Error::other("child pid did not fit in pid_t"));
169+
}
170+
};
171+
Ok(Self {
172+
child,
173+
process_group,
174+
reaped: false,
175+
})
176+
}
177+
178+
fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
179+
let status = self.child.try_wait()?;
180+
if status.is_some() {
181+
self.reaped = true;
182+
}
183+
Ok(status)
184+
}
185+
186+
fn terminate_group(&self) {
187+
// SAFETY: a negative pid addresses the dedicated process group created by process_group.
188+
let _ = unsafe { libc::kill(-self.process_group, libc::SIGKILL) };
189+
}
190+
191+
fn terminate_and_reap(&mut self) {
192+
self.terminate_group();
193+
while !self.reaped {
194+
match self.child.wait() {
195+
Ok(_) => self.reaped = true,
196+
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
197+
Err(_) => break,
198+
}
199+
}
200+
}
201+
}
202+
203+
impl Drop for OwnedChild {
204+
fn drop(&mut self) {
205+
self.terminate_and_reap();
206+
}
207+
}
208+
209+
#[cfg(test)]
210+
mod tests {
211+
use std::fs;
212+
use std::os::unix::fs::PermissionsExt as _;
213+
use std::path::PathBuf;
214+
215+
use tempfile::TempDir;
216+
217+
use super::*;
218+
219+
#[test]
220+
fn discovers_a_sensitive_authorization_header() {
221+
let (_temporary, executable) = executable_fixture("printf '%s\\n' 'fixture-token'");
222+
let authorization =
223+
discover_authorization(&executable, Instant::now() + Duration::from_secs(5)).unwrap();
224+
225+
assert!(authorization.is_sensitive());
226+
assert_eq!(authorization.as_bytes(), b"Bearer fixture-token");
227+
}
228+
229+
#[test]
230+
fn rejects_oversized_and_failed_credentials() {
231+
let oversized = format!("printf '{}'", "x".repeat(MAX_TOKEN_BYTES + 1));
232+
let (_temporary, executable) = executable_fixture(&oversized);
233+
assert!(
234+
discover_authorization(&executable, Instant::now() + Duration::from_secs(5)).is_none()
235+
);
236+
237+
let (_temporary, executable) = executable_fixture("printf '%s\\n' 'ignored-token'; exit 7");
238+
assert!(
239+
discover_authorization(&executable, Instant::now() + Duration::from_secs(5)).is_none()
240+
);
241+
}
242+
243+
#[test]
244+
fn deadline_terminates_the_owned_process_group() {
245+
let (_temporary, executable) = executable_fixture(
246+
r#"
247+
(
248+
trap '' TERM
249+
while :; do sleep 10; done
250+
) &
251+
descendant=$!
252+
printf '%s %s\n' "$$" "$descendant" > "$0.pids"
253+
wait "$descendant"
254+
"#,
255+
);
256+
let started = Instant::now();
257+
assert!(
258+
discover_authorization(&executable, started + Duration::from_millis(200),).is_none()
259+
);
260+
assert!(started.elapsed() < Duration::from_secs(1));
261+
262+
let pids = fs::read_to_string(format!("{}.pids", executable.display())).unwrap();
263+
let pids = pids
264+
.split_whitespace()
265+
.map(|pid| pid.parse::<i32>().unwrap())
266+
.collect::<Vec<_>>();
267+
let cleanup_deadline = Instant::now() + Duration::from_secs(1);
268+
while pids.iter().copied().any(process_is_running) && Instant::now() < cleanup_deadline {
269+
std::thread::sleep(Duration::from_millis(10));
270+
}
271+
assert!(!pids.iter().copied().any(process_is_running));
272+
}
273+
274+
fn executable_fixture(body: &str) -> (TempDir, PathBuf) {
275+
let temporary = tempfile::tempdir().unwrap();
276+
let executable = temporary.path().join("gh");
277+
fs::write(&executable, format!("#!/bin/sh\nset -eu\n{body}\n")).unwrap();
278+
let mut permissions = fs::metadata(&executable).unwrap().permissions();
279+
permissions.set_mode(0o700);
280+
fs::set_permissions(&executable, permissions).unwrap();
281+
(temporary, executable)
282+
}
283+
284+
fn process_is_running(pid: i32) -> bool {
285+
let Ok(stat) = fs::read_to_string(format!("/proc/{pid}/stat")) else {
286+
return false;
287+
};
288+
stat.rsplit_once(") ")
289+
.and_then(|(_, fields)| fields.chars().next())
290+
.is_some_and(|state| !matches!(state, 'Z' | 'X'))
291+
}
292+
}

0 commit comments

Comments
 (0)