Compare commits

..
Author SHA1 Message Date
Paul Bakaus c2c4372abf Set registered VS Code publisher identity
Use renaissance-geek for the Impeccable extension and guard the publisher identity in packaging tests.

AI assistance: Codex, under maintainer direction.
2026-09-07 12:15:41 -07:00
12 changed files with 8 additions and 316 deletions
Generated
+1 -78
View File
@@ -111,22 +111,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -606,14 +590,12 @@ dependencies = [
"impeccable-core",
"once_cell",
"regex",
"rustls-native-certs",
"serde",
"serde_json",
"sha2",
"tiny_http",
"unicode-normalization",
"ureq",
"webpki-roots 1.0.9",
]
[[package]]
@@ -848,12 +830,6 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -1087,7 +1063,7 @@ dependencies = [
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@@ -1120,18 +1096,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
@@ -1158,15 +1122,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -1187,29 +1142,6 @@ dependencies = [
"tendril",
]
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "selectors"
version = "0.38.0"
@@ -1671,15 +1603,6 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
-1
View File
@@ -81,7 +81,6 @@ impeccable detect [options] [file-or-dir-or-url...]
- Node.js 22.18+ to run `npx impeccable`. The engine itself is a self-contained binary and needs no runtime; the skill installed into your harness calls it directly.
- For URL scans, an installed Chrome, Chromium, or Edge (set `IMPECCABLE_BROWSER` to point at one).
- Behind a TLS-inspecting proxy, downloads trust your OS certificate store as well as the bundled Mozilla roots. Set `SSL_CERT_FILE` or `SSL_CERT_DIR` to use a specific CA bundle instead.
Binary lookup order: `IMPECCABLE_BIN`, the platform package, `~/.impeccable/bin/<version>/`, then a download of the pinned version into that cache. Set `IMPECCABLE_BIN` to a local build to skip all of that.
-2
View File
@@ -21,7 +21,5 @@ once_cell = { workspace = true }
sha2 = "0.10"
flate2 = { version = "1", default-features = false, features = ["zlib-rs"] }
ureq = { version = "2", default-features = false, features = ["tls", "json"] }
rustls-native-certs = "0.8"
webpki-roots = "1"
tiny_http = "0.12"
unicode-normalization = "0.1.25"
+1 -1
View File
@@ -47,7 +47,7 @@ fn card_base(env: &Env) -> String {
}
fn agent(timeout: Duration) -> ureq::Agent {
crate::http::agent_builder().timeout_connect(timeout).timeout(timeout).build()
ureq::AgentBuilder::new().timeout_connect(timeout).timeout(timeout).build()
}
/// URLSearchParams serialization (application/x-www-form-urlencoded).
+1 -1
View File
@@ -468,7 +468,7 @@ fn fetch_latest_skill_version(env: &Env) -> Option<String> {
.cloned()
.unwrap_or_else(|| "https://impeccable.style".to_string());
let host = host.strip_suffix('/').unwrap_or(&host).to_string();
let agent = crate::http::agent_builder()
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_millis(FETCH_TIMEOUT_MS))
.build();
let res = agent.get(&format!("{}/api/version", host)).call().ok()?;
+1 -90
View File
@@ -167,11 +167,7 @@ fn walk(root: &str, dir: &str, depth: usize, hits: &mut Hits) {
let Ok(bytes) = std::fs::read(&abs) else { continue };
let slice = if bytes.len() > MAX_READ_BYTES { &bytes[..MAX_READ_BYTES] } else { &bytes[..] };
let body = String::from_utf8_lossy(slice);
// Candidate patterns use '/', while native Windows relative paths
// use '\\'. Normalize once for classification and portable signals;
// to_posix preserves literal backslashes in Unix filenames.
let rel = jsp::to_posix(&jsp::relative("/", root, &abs));
visit(root, &abs, &rel, &body, hits);
visit(root, &abs, &jsp::relative("/", root, &abs), &body, hits);
}
}
@@ -201,88 +197,3 @@ pub fn run(_args: &[String], io: &mut Io) -> i32 {
io.out(&format!("{}\n", json_pretty(&v)));
0
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT_FIXTURE: AtomicUsize = AtomicUsize::new(0);
struct Fixture(PathBuf);
impl Fixture {
fn new() -> Self {
loop {
let root = std::env::temp_dir().join(format!(
"impeccable-csp-761-{}-{}",
std::process::id(), NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed),
));
match std::fs::create_dir(&root) {
Ok(()) => return Self(root),
// Never reuse or remove files left by another run.
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => panic!("create CSP fixture: {e}"),
}
}
}
fn scan(&self, path: &str, body: &str) -> Value {
let file = self.0.join(path);
std::fs::create_dir_all(file.parent().unwrap()).unwrap();
std::fs::write(file, body).unwrap();
detect_csp(self.0.to_str().unwrap())
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn nested_csp_candidates_use_portable_paths() {
// Exercise the filesystem walker and native path.relative semantics,
// not just regexes with pre-normalized input. Windows CI reproduces #761.
for (path, body, shape) in [
("packages/app/src/security/csp.ts", "buildCSPConfig()", "append-arrays"),
("packages/app/src/next-config.ts", "createBaseNextConfig()", "append-arrays"),
("apps/web/svelte.config.js", "kit: { csp: { directives: {} } }", "append-arrays"),
("apps/web/nuxt.config.ts", "'nuxt-security'; contentSecurityPolicy", "append-arrays"),
("apps/web/next.config.mjs", "'Content-Security-Policy': 'script-src self; connect-src self'", "append-string"),
("next.config.mjs", "'Content-Security-Policy': 'script-src self; connect-src self'", "append-string"),
("apps/web/src/middleware.ts", "headers.set('Content-Security-Policy', policy)", "middleware"),
("apps/web/src/layout.astro", "<meta http-equiv='Content-Security-Policy'>", "meta-tag"),
] {
assert_eq!(Fixture::new().scan(path, body), serde_json::json!({
"shape": shape, "signals": [path],
}), "{path}");
}
}
#[test]
fn unrelated_nested_files_are_not_csp_candidates() {
for (path, body) in [
("packages/app/src/utils/csp.ts", "buildCSPConfig()"),
("apps/web/not-svelte.config.js", "kit: { csp: { directives: {} } }"),
("apps/web/next.config.mjs", "'Content-Security-Policy': 'script-src self'"),
] {
assert_eq!(Fixture::new().scan(path, body), serde_json::json!({
"shape": null, "signals": [],
}), "{path}");
}
}
#[cfg(unix)]
#[test]
fn posix_backslashes_remain_literal_filename_characters() {
let result = Fixture::new().scan(
"packages/app/src/config\\notes.ts", "buildCSPConfig()",
);
assert_eq!(result, serde_json::json!({
"shape": "append-arrays", "signals": ["packages/app/src/config\\notes.ts"],
}));
}
}
+1 -1
View File
@@ -294,7 +294,7 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
}
}
}
let agent = crate::http::agent_builder().build();
let agent = ureq::AgentBuilder::new().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();
-104
View File
@@ -1,104 +0,0 @@
//! One TLS trust configuration for every HTTPS request the engine makes.
//!
//! `ureq`'s default rustls config trusts only the Mozilla roots compiled in
//! through `webpki-roots`. On a machine where an endpoint security agent
//! inspects TLS (Aikido, Zscaler, Netskope: routine in managed corporate
//! setups), every connection terminates at a proxy whose root lives in the
//! OS trust store and nowhere else, so `update` and `install` failed with
//! `invalid peer certificate: UnknownIssuer` while curl and npm on the same
//! machine succeeded (#757).
//!
//! The store built here is the union of the OS trust store
//! (`rustls-native-certs`: the macOS Keychain, the Windows store, the
//! OpenSSL paths on Linux) and the bundled Mozilla roots. A union, not a
//! replacement: a container without `ca-certificates`, or a store that
//! fails to load, verifies against the bundled roots exactly as before.
//! `SSL_CERT_FILE` / `SSL_CERT_DIR` stand in for the OS store, as they do
//! for OpenSSL and curl; the bundled roots stay either way.
use std::sync::Arc;
use once_cell::sync::Lazy;
use ureq::rustls::pki_types::CertificateDer;
use ureq::rustls::{self, ClientConfig, RootCertStore};
/// `ureq::AgentBuilder::new()` with the engine's trust store installed.
/// Every HTTPS call site builds its agent from this; the plain-HTTP calls
/// to the live server on localhost do not need it.
pub fn agent_builder() -> ureq::AgentBuilder {
ureq::AgentBuilder::new().tls_config(tls_config())
}
fn tls_config() -> Arc<ClientConfig> {
static CONFIG: Lazy<Arc<ClientConfig>> = Lazy::new(|| {
// Mirrors ureq's own default config (provider and protocol versions);
// only the root store differs.
let config =
ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
.with_protocol_versions(&[&rustls::version::TLS12, &rustls::version::TLS13])
.expect("the ring provider supports TLS 1.2 and 1.3")
.with_root_certificates(root_store(rustls_native_certs::load_native_certs().certs))
.with_no_client_auth();
Arc::new(config)
});
CONFIG.clone()
}
/// The bundled Mozilla roots plus every parsable certificate in `native`.
/// Unparsable entries are dropped, so one broken certificate in the OS
/// store cannot take the bundled roots down with it.
fn root_store(native: Vec<CertificateDer<'static>>) -> RootCertStore {
let mut store = RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
};
store.add_parsable_certificates(native);
store
}
#[cfg(test)]
mod tests {
use super::*;
use ureq::rustls::pki_types::pem::PemObject;
/// Self-signed CA minted for this test (P-256, v3, CA:TRUE): the shape
/// of the root a TLS-inspecting proxy installs into the OS store.
const PROXY_ROOT_PEM: &str = "-----BEGIN CERTIFICATE-----
MIIBdTCCARugAwIBAgIJANhTZvQvv7HJMAoGCCqGSM49BAMCMB0xGzAZBgNVBAMM
EmltcGVjY2FibGUgdGVzdCBDQTAgFw0yNjA5MDcwNjQxMjdaGA8yMTI2MDgxNDA2
NDEyN1owHTEbMBkGA1UEAwwSaW1wZWNjYWJsZSB0ZXN0IENBMFkwEwYHKoZIzj0C
AQYIKoZIzj0DAQcDQgAEYVZtCOXaZsY71/0Roy62iBVcyx8UfMDkPbEbf/IEw5Bm
yNBfKTFS/8FbRBMWHXOwNE0Ns1BLVOB1oQ1XFC5Bz6NCMEAwDwYDVR0TAQH/BAUw
AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFONzBxi7ewOfuP6cBIIqsxu
3pEiMAoGCCqGSM49BAMCA0gAMEUCIQD98Q0ZRe8ceuopnUwQKYleZd5IzfWhhpmO
tB0WGTOG3QIgdJa8gBPU9Y6WsrursItsnUeGTYHKDCZZ6MjlekLFuoc=
-----END CERTIFICATE-----
";
fn bundled() -> usize {
webpki_roots::TLS_SERVER_ROOTS.len()
}
#[test]
fn bundled_roots_alone_when_the_os_store_is_empty() {
assert_eq!(root_store(Vec::new()).len(), bundled());
}
#[test]
fn os_store_root_joins_the_bundled_roots() {
let proxy = CertificateDer::from_pem_slice(PROXY_ROOT_PEM.as_bytes()).unwrap();
assert_eq!(root_store(vec![proxy]).len(), bundled() + 1);
}
#[test]
fn unparsable_os_store_entry_is_dropped() {
let junk = CertificateDer::from(b"not a certificate".to_vec());
assert_eq!(root_store(vec![junk]).len(), bundled());
}
#[test]
fn agent_builds_from_this_hosts_store() {
// Runs the real rustls-native-certs load: it must not panic, and the
// shared config must be accepted by a ureq agent.
let _agent = agent_builder().build();
}
}
-1
View File
@@ -5,7 +5,6 @@
pub mod jsp;
pub mod util;
pub mod url;
pub mod http;
pub mod provider;
pub mod hook_markers;
pub mod target_args;
+2 -2
View File
@@ -49,7 +49,7 @@ const RATIO_GUARD_FLOOR: u64 = 1024 * 1024;
/// through `download_file`, which streams to disk. Reads are capped at
/// [`MAX_DOWNLOAD_BYTES`]; a longer response is an error, not a truncation.
pub fn download(url: &str) -> Result<Vec<u8>, String> {
let agent = impeccable_context::http::agent_builder()
let agent = ureq::AgentBuilder::new()
.timeout_connect(std::time::Duration::from_secs(30))
.build();
match agent.get(url).call() {
@@ -79,7 +79,7 @@ pub struct FetchResponse {
}
fn ureq_fetch(url: &str) -> Result<FetchResponse, String> {
let agent = impeccable_context::http::agent_builder()
let agent = ureq::AgentBuilder::new()
.timeout_connect(std::time::Duration::from_secs(30))
.timeout(std::time::Duration::from_secs(120))
.redirects(0)
+1 -33
View File
@@ -35,10 +35,6 @@ impl Prompt {
self.stdin_tty && self.stdout_tty && cfg!(unix)
}
fn uses_tty_readline(&self, io: &Io) -> bool {
cfg!(unix) && self.stdout_tty && io.env("TERM") != Some("dumb")
}
fn ansi(&self, open: &str, close: &str, value: &str) -> String {
if self.style {
format!("{open}{value}{close}")
@@ -74,7 +70,7 @@ impl Prompt {
let next = self.piped.as_mut().and_then(|v| v.pop()).unwrap_or_default();
return Ok(next.trim().to_lowercase());
}
if self.uses_tty_readline(io) {
if self.stdout_tty && io.env("TERM") != Some("dumb") {
return self.tty_readline(io, question);
}
io.out(question);
@@ -628,9 +624,6 @@ fn terminal_rows() -> Option<u16> {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::path::PathBuf;
use super::*;
#[test]
@@ -644,29 +637,4 @@ mod tests {
assert_eq!(visible_window(15, 16, 10), (6, 16));
assert_eq!(visible_window(2, 3, 10), (0, 3));
}
fn tty_prompt() -> Prompt {
Prompt { stdin_tty: true, stdout_tty: true, style: false, piped: None }
}
#[test]
fn ask_uses_raw_readline_only_on_unix() {
let prompt = tty_prompt();
let (io, _) = Io::captured("", PathBuf::from("."), HashMap::new());
assert_eq!(prompt.uses_tty_readline(&io), cfg!(unix));
}
#[cfg(not(unix))]
#[test]
fn ask_on_windows_tty_does_not_throw_unsupported() {
// Line fallback reads process stdin. Skip on a live console so the
// test cannot hang; CI pipes EOF and gets Ok("").
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return;
}
let mut prompt = tty_prompt();
let (mut io, _) = Io::captured("", PathBuf::from("."), HashMap::new());
let result = prompt.ask(&mut io, "Update skills in 1 provider folder(s)? (Y/n) ");
assert_eq!(result, Ok(String::new()));
}
}
-2
View File
@@ -352,8 +352,6 @@ content, and failures fetching either asset exit nonzero, including when
installed skill or hook files. Explicit `IMPECCABLE_BUNDLE_PATH` and `link`
retain their local-development trust behavior. See [bundle signing](BUNDLE-SIGNING.md).
**TLS trust (#757):** every HTTPS request the engine makes (bundle and signature downloads, `/api/version`, `/api/commands`, the roll API, image generation) verifies against the OS trust store plus the bundled Mozilla roots, built once in `crates/context/src/http.rs`. `SSL_CERT_FILE` / `SSL_CERT_DIR` replace the OS store, never the bundled roots. The live server calls on localhost are plain HTTP and unaffected.
- **Invoked from**: README.md ("npx impeccable install / update"), README.npm.md Quick Start (`npx impeccable skills install`, `... install -y --providers=claude,codex --scope=project`, `... update`, `... install --no-hooks`, `... link --source=.impeccable --providers=claude,cursor`, `... skills help`), `README.md:360` (hook consent explanation).
- `run(args)`: `args[0]``undefined|help|--help|-h``showHelp()`; `install``install(rest)`; `link`; `update`; `check` (ignores flags); else `stderr> Unknown skills command: ${sub}` + `Run 'impeccable --help' for available commands.`, `exit 1`.
- Constants: `API_BASE = 'https://impeccable.style'`; `PROVIDER_DIRS = ['.claude','.cursor','.dsh','.gemini','.agents','.agent','.github','.grok','.hermes','.kiro','.opencode','.pi','.qoder','.trae','.trae-cn','.rovodev','.vibe']`; aliases (`agent``.agent`, `agents`/`codex``.agents`, `antigravity``.agent`, `claude`/`claude-code``.claude`, `copilot`/`github``.github`, `cursor`, `deepseek`/`deepseek-harness`/`dsh``.dsh`, `gemini`, `grok`/`grok-build`/`xai``.grok`, `hermes`, `kiro`, `opencode`, `pi`, `qoder`, `rovo-dev`/`rovodev``.rovodev`, `trae`, `trae-cn`, `vibe`); leading `.` stripped and lowercased before alias lookup; a literal PROVIDER_DIR value is accepted as-is. `DEFAULT_TARGETS = ['.claude','.agents']`. User-scope skill dir overrides: `.agent``~/.gemini/config/skills`, `.dsh``$DSH_HOME/skills` (only when DSH_HOME under home) else `~/.dsh/skills`, `.hermes``$HERMES_HOME/skills` (only when HERMES_HOME under home) else `~/.hermes/skills`, `.pi``~/.pi/agent/skills`, `.opencode``$OPENCODE_CONFIG_DIR|$XDG_CONFIG_HOME/opencode|~/.config/opencode` + `/skills`; others `~/<provider>/skills`. Project scope: `<root>/<provider>/skills`.