Files
pbakaus_impeccable/crates/context/src/generate_image.rs
T
bd6964c35b Trust the OS certificate store for engine HTTPS requests (#757) (#759)
The engine verified TLS against the Mozilla roots bundled through
webpki-roots only, so behind a TLS-inspecting proxy (Aikido, Zscaler,
Netskope) whose root lives in the OS trust store, `impeccable update`
and `install` failed with `invalid peer certificate: UnknownIssuer`
while curl and npm on the same machine succeeded.

crates/context/src/http.rs builds one rustls ClientConfig per process:
the OS trust store (rustls-native-certs: Keychain, Windows store, the
OpenSSL paths on Linux) merged with the bundled roots. A union, not a
replacement, so a container without ca-certificates or a store that
fails to load still verifies exactly as before. SSL_CERT_FILE and
SSL_CERT_DIR replace the OS store the way they do for OpenSSL and curl.
Every HTTPS call site (bundle and signature downloads, /api/version,
/api/commands, the roll API, image generation) builds its agent from
this module; the plain-HTTP live-server calls on localhost are
untouched.

Verified against a local HTTPS server signed by a throwaway CA: trusted
through SSL_CERT_FILE the update check reaches it; without it the same
server is rejected as UnknownIssuer; with SSL_CERT_FILE pointing at that
CA or at a missing file, impeccable.style still verifies through the
bundled roots. cargo test --workspace and the oracle corpus (832) pass.

Written with AI assistance (Claude Code).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 13:15:18 -07:00

433 lines
16 KiB
Rust

//! JS: generate-image.mjs -> `impeccable generate-image`
use crate::jsp;
use crate::util::{iso_now, json_pretty, node_read_error, utf16_len, Env};
use impeccable_common::Io;
use serde_json::{Map, Value};
use std::io::Write;
fn arg(args: &[String], name: &str) -> Option<String> {
let i = args.iter().position(|a| a == &format!("--{}", name))?;
let v = args.get(i + 1)?;
if !v.is_empty() && !v.starts_with("--") {
Some(v.clone())
} else {
None
}
}
fn hash32(s: &str) -> u32 {
let mut h: u32 = 0x811c9dc5;
for cu in s.encode_utf16() {
h ^= cu as u32;
h = h.wrapping_mul(0x01000193);
}
h
}
fn hsl_to_rgb(h_deg: f64, s: f64, l: f64) -> [u8; 3] {
let h = ((h_deg % 360.0) + 360.0) % 360.0 / 360.0;
let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
let p = 2.0 * l - q;
let hue = |t: f64| -> f64 {
let mut tt = t;
if tt < 0.0 {
tt += 1.0;
}
if tt > 1.0 {
tt -= 1.0;
}
if tt < 1.0 / 6.0 {
return p + (q - p) * 6.0 * tt;
}
if tt < 1.0 / 2.0 {
return q;
}
if tt < 2.0 / 3.0 {
return p + (q - p) * (2.0 / 3.0 - tt) * 6.0;
}
p
};
let round = |c: f64| -> u8 { js_round(c * 255.0) as u8 };
[round(hue(h + 1.0 / 3.0)), round(hue(h)), round(hue(h - 1.0 / 3.0))]
}
/// Math.round: half up toward +inf
fn js_round(x: f64) -> f64 {
(x + 0.5).floor()
}
fn to_hex(c: [u8; 3]) -> String {
format!("#{:02x}{:02x}{:02x}", c[0], c[1], c[2])
}
fn palette(prompt: &str) -> Vec<[u8; 3]> {
let h = hash32(prompt);
let base = (h % 360) as f64;
let bands = 2 + ((h >> 9) % 2) as usize;
let spread = (40 + (h >> 3) % 120) as f64;
let mut out = Vec::new();
for i in 0..bands {
let hue = base + i as f64 * spread;
let light = 0.32 + ((h >> (i * 5)) % 40) as f64 / 100.0;
out.push(hsl_to_rgb(hue, 0.55, light));
}
out
}
fn collapse_ws(s: &str) -> String {
// .replace(/\s+/g, ' ').trim()
let mut out = String::new();
let mut in_ws = false;
for c in s.chars() {
if c.is_whitespace() || c == '\u{FEFF}' {
if !in_ws {
out.push(' ');
in_ws = true;
}
} else {
in_ws = false;
out.push(c);
}
}
crate::util::js_trim(&out).to_string()
}
fn num(v: f64) -> String {
crate::util::js_number_to_string(v)
}
fn svg_fake(prompt: &str, w: f64, h: f64) -> String {
let colors: Vec<String> = palette(prompt).into_iter().map(to_hex).collect();
let n = colors.len();
let stops: String = colors
.iter()
.enumerate()
.map(|(i, c)| format!("<stop offset=\"{}%\" stop-color=\"{}\"/>", num(js_round(i as f64 / (n as f64 - 1.0) * 100.0)), c))
.collect();
let per_line = (12.0f64).max((w / 26.0).floor()) as usize;
let words: Vec<&str> = {
let collapsed = collapse_ws(prompt);
// split(' ') on the collapsed string; leak for lifetime simplicity
Box::leak(collapsed.into_boxed_str()).split(' ').collect()
};
let mut lines: Vec<String> = Vec::new();
let mut cur = String::new();
for word in words {
let candidate = crate::util::js_trim(&format!("{} {}", cur, word)).to_string();
if utf16_len(&candidate) > per_line {
if !cur.is_empty() {
lines.push(cur.clone());
}
cur = word.to_string();
} else {
cur = candidate;
}
if lines.len() >= 10 {
break;
}
}
if !cur.is_empty() && lines.len() < 11 {
lines.push(cur);
}
let escape = |s: &str| s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;");
let font_size = js_round(w / 24.0);
let start_y = h / 2.0 - ((lines.len() as f64 - 1.0) * font_size * 1.3) / 2.0;
let text: String = lines
.iter()
.enumerate()
.map(|(i, line)| {
format!(
"<text x=\"{}\" y=\"{}\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"{}\" fill=\"#ffffff\" text-anchor=\"middle\" dominant-baseline=\"middle\">{}</text>",
num(w / 2.0),
num(js_round(start_y + i as f64 * font_size * 1.3)),
num(font_size),
escape(line)
)
})
.collect();
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\">\n <defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">{stops}</linearGradient></defs>\n <rect width=\"{w}\" height=\"{h}\" fill=\"url(#g)\"/>\n <rect x=\"0\" y=\"0\" width=\"{w}\" height=\"{h}\" fill=\"#000000\" fill-opacity=\"0.22\"/>\n {text}\n <rect x=\"{x1}\" y=\"{y1}\" width=\"{bw}\" height=\"{bh}\" fill=\"#000000\" fill-opacity=\"0.55\"/>\n <text x=\"{tx}\" y=\"{ty}\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"{fs}\" letter-spacing=\"2\" fill=\"#ffffff\" text-anchor=\"middle\" dominant-baseline=\"middle\">SYNTHETIC COMP</text>\n</svg>\n",
w = num(w),
h = num(h),
stops = stops,
text = text,
x1 = num(w - js_round(w / 4.2)),
y1 = num(h - js_round(h / 16.0)),
bw = num(js_round(w / 4.2)),
bh = num(js_round(h / 16.0)),
tx = num(w - js_round(w / 8.4)),
ty = num(h - js_round(h / 32.0)),
fs = num(js_round(w / 60.0)),
)
}
fn crc32(data: &[u8]) -> u32 {
let mut c = 0xffffffffu32;
for b in data {
c ^= *b as u32;
for _ in 0..8 {
c = if c & 1 == 1 { 0xedb88320 ^ (c >> 1) } else { c >> 1 };
}
}
c ^ 0xffffffff
}
fn png_chunk(ty: &[u8], data: &[u8]) -> Vec<u8> {
let mut body = ty.to_vec();
body.extend_from_slice(data);
let mut out = Vec::new();
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
out.extend_from_slice(&body);
out.extend_from_slice(&crc32(&body).to_be_bytes());
out
}
fn png_fake(prompt: &str, w: usize, h: usize) -> Vec<u8> {
let colors = palette(prompt);
let band_h = (h as f64 / colors.len() as f64).ceil() as usize;
let stride = w * 3;
let mut raw = vec![0u8; h * (stride + 1)];
for y in 0..h {
let row = y * (stride + 1);
raw[row] = 0;
let idx = (colors.len() - 1).min(if band_h == 0 { 0 } else { y / band_h });
let [r, g, b] = colors[idx];
for x in 0..w {
let p = row + 1 + x * 3;
raw[p] = r;
raw[p + 1] = g;
raw[p + 2] = b;
}
}
let mut ihdr = vec![0u8; 13];
ihdr[..4].copy_from_slice(&(w as u32).to_be_bytes());
ihdr[4..8].copy_from_slice(&(h as u32).to_be_bytes());
ihdr[8] = 8;
ihdr[9] = 2;
let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(9));
let _ = enc.write_all(&raw);
let idat = enc.finish().unwrap_or_default();
let mut text = b"Comment".to_vec();
text.push(0);
// latin1: chars > 0xff become their low byte in Node's 'latin1' encoding
for c in format!("SYNTHETIC COMP: {}", collapse_ws(prompt)).encode_utf16() {
text.push((c & 0xff) as u8);
}
let mut out = vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
out.extend(png_chunk(b"IHDR", &ihdr));
out.extend(png_chunk(b"tEXt", &text));
out.extend(png_chunk(b"IDAT", &idat));
out.extend(png_chunk(b"IEND", &[]));
out
}
fn parse_size(s: &str) -> (usize, usize) {
if let Some((a, b)) = s.split_once('x') {
if !a.is_empty() && !b.is_empty() && a.chars().all(|c| c.is_ascii_digit()) && b.chars().all(|c| c.is_ascii_digit()) {
return (a.parse().unwrap_or(1536), b.parse().unwrap_or(1024));
}
}
(1536, 1024)
}
pub fn run(args: &[String], io: &mut Io) -> i32 {
let cwd = io.cwd.to_string_lossy().into_owned();
let env: Env = io.env.clone();
let abs = |p: &str| jsp::resolve(&cwd, &[p]);
let read_prompt_file = |io: &mut Io, pf: &str| -> Result<String, i32> {
match std::fs::read(abs(pf)) {
Ok(b) => Ok(String::from_utf8_lossy(&b).into_owned()),
Err(e) => {
io.err(&format!("Error: {}\n", node_read_error(pf, &e)));
Err(1)
}
}
};
if env.get("IMPECCABLE_IMAGE_GEN_FAKE").map(|v| !v.is_empty()).unwrap_or(false) {
let prompt = match arg(args, "prompt-file") {
Some(pf) => match read_prompt_file(io, &pf) {
Ok(p) => Some(p),
Err(c) => return c,
},
None => arg(args, "prompt"),
};
let out = arg(args, "out");
let (Some(prompt), Some(out)) = (prompt.filter(|p| !p.is_empty()), out) else {
io.err("generate-image: --prompt (or --prompt-file) and --out are required.\n");
return 1;
};
let (w, h) = parse_size(&arg(args, "size").unwrap_or_else(|| "1536x1024".into()));
let bytes = if out.ends_with(".svg") { svg_fake(&prompt, w as f64, h as f64).into_bytes() } else { png_fake(&prompt, w, h) };
if let Err(e) = std::fs::write(abs(&out), bytes) {
io.err(&format!("Error: {}\n", node_read_error(&out, &e)));
return 1;
}
io.out(&format!("IMAGE: {} ({}x{}, fake synthetic comp, $0.00, no API call)\n", out, w, h));
return 0;
}
let Some(key) = env.get("OPENAI_API_KEY").filter(|k| !k.is_empty()).cloned() else {
io.err("generate-image: OPENAI_API_KEY is not set; use the harness-native image tool instead.\n");
return 1;
};
let prompt = match arg(args, "prompt-file") {
Some(pf) => match read_prompt_file(io, &pf) {
Ok(p) => Some(p),
Err(c) => return c,
},
None => arg(args, "prompt"),
};
let out = arg(args, "out");
let (Some(prompt), Some(out)) = (prompt.filter(|p| !p.is_empty()), out) else {
io.err("generate-image: --prompt (or --prompt-file) and --out are required.\n");
return 1;
};
let size = arg(args, "size").unwrap_or_else(|| "1536x1024".into());
let quality = arg(args, "quality").unwrap_or_else(|| "medium".into());
let mut refs: Vec<String> = Vec::new();
for i in 0..args.len() {
if args[i] == "--ref" {
if let Some(n) = args.get(i + 1) {
if !n.is_empty() && !n.starts_with("--") {
refs.push(n.clone());
}
}
}
}
let agent = crate::http::agent_builder().build();
let response = if !refs.is_empty() {
let boundary = format!("----impeccable{:x}", crate::util::now_ms() as u64);
let mut body: Vec<u8> = Vec::new();
let mut field = |name: &str, value: &str| {
body.extend_from_slice(format!("--{}\r\nContent-Disposition: form-data; name=\"{}\"\r\n\r\n{}\r\n", boundary, name, value).as_bytes());
};
field("model", "gpt-image-2");
field("prompt", &prompt);
field("size", &size);
field("quality", &quality);
field("n", "1");
for r in &refs {
let bytes = match std::fs::read(abs(r)) {
Ok(b) => b,
Err(e) => {
io.err(&format!("Error: {}\n", node_read_error(r, &e)));
return 1;
}
};
let ty = if r.ends_with(".png") {
"image/png"
} else if r.ends_with(".webp") {
"image/webp"
} else {
"image/jpeg"
};
let filename = r.rsplit('/').next().unwrap_or(r);
body.extend_from_slice(
format!("--{}\r\nContent-Disposition: form-data; name=\"image[]\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", boundary, filename, ty).as_bytes(),
);
body.extend_from_slice(&bytes);
body.extend_from_slice(b"\r\n");
}
body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
agent
.post("https://api.openai.com/v1/images/edits")
.set("Authorization", &format!("Bearer {}", key))
.set("Content-Type", &format!("multipart/form-data; boundary={}", boundary))
.send_bytes(&body)
} else {
let mut m = Map::new();
m.insert("model".into(), Value::String("gpt-image-2".into()));
m.insert("prompt".into(), Value::String(prompt.clone()));
m.insert("size".into(), Value::String(size.clone()));
m.insert("quality".into(), Value::String(quality.clone()));
m.insert("n".into(), Value::from(1));
agent
.post("https://api.openai.com/v1/images/generations")
.set("Authorization", &format!("Bearer {}", key))
.set("content-type", "application/json")
.send_string(&serde_json::to_string(&Value::Object(m)).unwrap())
};
let (status, text) = match response {
Ok(r) => {
let st = r.status();
(st, r.into_string().unwrap_or_default())
}
Err(ureq::Error::Status(code, r)) => (code, r.into_string().unwrap_or_default()),
Err(e) => {
io.err(&format!("TypeError: fetch failed: {}\n", e));
return 1;
}
};
if !(200..300).contains(&status) {
let snippet: String = text.chars().take(300).collect();
io.err(&format!("generate-image: API error {}: {}\n", status, snippet));
return 1;
}
let json: Value = serde_json::from_str(&text).unwrap_or(Value::Null);
let b64 = json.get("data").and_then(|d| d.get(0)).and_then(|d| d.get("b64_json")).and_then(|b| b.as_str()).filter(|s| !s.is_empty());
let Some(b64) = b64 else {
io.err("generate-image: no image in response\n");
return 1;
};
let bytes = base64_decode(b64);
let _ = std::fs::write(abs(&out), bytes);
// best-effort embed + sidecar
// JS-PARITY: generate-image.mjs#676 reports whether the embed actually
// succeeded. The install-path-with-spaces half of #676 is a JS-only
// subprocess concern (fileURLToPath vs URL.pathname); the engine embeds
// in-process, so only the success tracking and message carry over here.
let embedded;
{
let mut sub_io = Io::captured("", io.cwd.clone(), io.env.clone()).0;
let ret = crate::embed_prompt::run(&[out.clone(), "--prompt".to_string(), prompt.clone()], &mut sub_io);
embedded = ret == 0;
if !embedded {
io.err("generate-image: failed to embed prompt in the image\n");
}
let mut m = Map::new();
m.insert("prompt".into(), Value::String(prompt.clone()));
m.insert("createdAt".into(), Value::String(iso_now()));
m.insert("tool".into(), Value::String("impeccable generate-image".into()));
m.insert("model".into(), Value::String("gpt-image-2".into()));
if !refs.is_empty() {
m.insert("refs".into(), Value::Array(refs.iter().cloned().map(Value::String).collect()));
}
let _ = std::fs::write(abs(&format!("{}.json", out)), json_pretty(&Value::Object(m)));
}
io.out(&format!(
"IMAGE: {} ({}, {}, gpt-image-2, billed to your OpenAI key); {} at {}.json\n",
out,
size,
quality,
if embedded { "prompt embedded + sidecar" } else { "sidecar" },
out
));
0
}
fn base64_decode(s: &str) -> Vec<u8> {
let table = |c: u8| -> Option<u32> {
match c {
b'A'..=b'Z' => Some((c - b'A') as u32),
b'a'..=b'z' => Some((c - b'a' + 26) as u32),
b'0'..=b'9' => Some((c - b'0' + 52) as u32),
b'+' | b'-' => Some(62),
b'/' | b'_' => Some(63),
_ => None,
}
};
let mut out = Vec::new();
let mut acc: u32 = 0;
let mut bits = 0;
for b in s.bytes() {
let Some(v) = table(b) else { continue };
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push(((acc >> bits) & 0xff) as u8);
}
}
out
}