Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions .github/workflows/peekaboo-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ jobs:
export PATH="$HOME/.cargo/bin:$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
mkdir -p "$REPO_DIR/artifacts/computer-use-local"
stale_run="$(cat "$REPO_DIR/artifacts/computer-use-local/.current-run" 2>/dev/null || true)"
stale_single_frame_recorders="$(
ps -axo pid=,command= |
awk '/ffmpeg/ && /-frames:v 1/ && !/-framerate/ { print $1 }'
)"
if [[ -n "$stale_single_frame_recorders" ]]; then
echo "Clearing stale one-frame ffmpeg capture process(es): $stale_single_frame_recorders"
kill $stale_single_frame_recorders >/dev/null 2>&1 || true
sleep 1
kill -9 $stale_single_frame_recorders >/dev/null 2>&1 || true
fi
pkill -TERM -f 'tools/computer-use-e2e/run-local\.mjs run-peekaboo-suite|tests/e2e/run\.sh|nixmac-e2e-provider|nixmac\.app/Contents/MacOS/nixmac|ffmpeg .*peekaboo-e2e' >/dev/null 2>&1 || true
sleep 2
pkill -KILL -f 'tools/computer-use-e2e/run-local\.mjs run-peekaboo-suite|tests/e2e/run\.sh|nixmac-e2e-provider|nixmac\.app/Contents/MacOS/nixmac|ffmpeg .*peekaboo-e2e' >/dev/null 2>&1 || true
Expand Down Expand Up @@ -456,19 +466,69 @@ jobs:
trusted_secret_scan_file="$FETCH_REPORT_DIR/trusted-secret-scan.json"
trusted_secret_scan_tmp="$(mktemp "${RUNNER_TEMP:-/tmp}/nixmac-trusted-secret-scan.XXXXXX.json")"
node --input-type=module - "$FETCH_REPORT_DIR" > "$trusted_secret_scan_tmp" <<'NODE'
import { readdirSync, readFileSync, lstatSync } from 'node:fs';
import { closeSync, lstatSync, openSync, readSync, readdirSync } from 'node:fs';
import path from 'node:path';
const root = process.argv[2];
const secretPattern = /(?:sk-[A-Za-z0-9_-]{16,}|xai-[A-Za-z0-9_-]{16,}|gh[opsu]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{22,}|xox[abprs]-[A-Za-z0-9-]{16,}|AKIA[0-9A-Z]{16}|Bearer\s+[A-Za-z0-9._-]{16,}|(?:OPENROUTER|OPENAI|ANTHROPIC|GROQ|XAI|MISTRAL|COHERE|GITHUB|SLACK|AWS)_API_KEY=(?!\[REDACTED\])[^\s"'<>]+)/i;
const secretPatternGlobal = new RegExp(secretPattern.source, `${secretPattern.flags.replace('g', '')}g`);
const TEXT_SNIFF_BYTES = 8192;
const TEXT_SCAN_CHUNK_BYTES = 65536;
// Larger than real token lengths, so chunk-boundary matches stay bounded-memory.
const TEXT_SCAN_OVERLAP_CHARS = 2048;
const violations = [];
let scannedPaths = 0;
let scannedFiles = 0;
function readSample(full, maxBytes = TEXT_SNIFF_BYTES) {
const fd = openSync(full, 'r');
try {
const buffer = Buffer.allocUnsafe(maxBytes);
const bytesRead = readSync(fd, buffer, 0, maxBytes, 0);
return buffer.subarray(0, bytesRead);
} finally {
closeSync(fd);
}
}
function looksLikeTextFile(full) {
const sample = readFileSync(full).subarray(0, 8192);
const sample = readSample(full);
if (sample.includes(0)) return false;
const suspiciousControlBytes = [...sample].filter((byte) => byte < 9 || (byte > 13 && byte < 32)).length;
return sample.length === 0 || suspiciousControlBytes / sample.length < 0.02;
}
function scanChunkForSecret(text, hasPotentialNextChunk) {
secretPatternGlobal.lastIndex = 0;
for (const match of text.matchAll(secretPatternGlobal)) {
const matchEnd = (match.index ?? 0) + match[0].length;
if (hasPotentialNextChunk && matchEnd === text.length) {
const maybeValue = match[0].split('=').at(-1) ?? '';
if ('[REDACTED]'.startsWith(maybeValue)) {
return { status: 'deferred-redaction', carryStart: match.index ?? 0 };
}
}
return { status: 'match', carryStart: null };
}
return { status: 'none', carryStart: null };
}
function textFileHasSecret(full) {
const fd = openSync(full, 'r');
const buffer = Buffer.allocUnsafe(TEXT_SCAN_CHUNK_BYTES);
let position = 0;
let carry = '';
try {
for (;;) {
const bytesRead = readSync(fd, buffer, 0, buffer.length, position);
if (bytesRead === 0) return false;
const text = carry + buffer.subarray(0, bytesRead).toString('utf8');
const scanStatus = scanChunkForSecret(text, bytesRead === buffer.length);
if (scanStatus.status === 'match') return true;
carry = scanStatus.status === 'deferred-redaction'
? text.slice(scanStatus.carryStart)
: text.slice(-TEXT_SCAN_OVERLAP_CHARS);
position += bytesRead;
}
} finally {
closeSync(fd);
}
}
function walk(dir) {
for (const entry of readdirSync(dir)) {
const full = path.join(dir, entry);
Expand All @@ -481,8 +541,7 @@ jobs:
walk(full);
} else if (stat.isFile() && looksLikeTextFile(full)) {
scannedFiles += 1;
const text = readFileSync(full, 'utf8');
if (secretPattern.test(text)) violations.push(`content:${relativePath}`);
if (textFileHasSecret(full)) violations.push(`content:${relativePath}`);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/native/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ tauri-plugin-updater = "2.0.0"
url = "2"

[target.'cfg(target_os = "macos")'.dependencies]
block = "0.1"
cocoa = "0.26"
objc = "0.2"

Expand Down
85 changes: 81 additions & 4 deletions apps/native/src-tauri/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ use crate::{rebuild, shared_types};
use std::process::Command;
use tauri::AppHandle;

fn e2e_mock_system_enabled() -> bool {
cfg!(debug_assertions) && crate::e2e_runtime::enabled("NIXMAC_E2E_MOCK_SYSTEM")
}

fn e2e_mock_host_attr() -> String {
crate::e2e_runtime::value("NIXMAC_E2E_HOST_ATTR").unwrap_or_else(|| "e2e-host".to_string())
}

/// Starts a streaming darwin-rebuild switch operation.
/// Progress is emitted via `darwin:apply:data` events, completion via `darwin:apply:end`.
#[tauri::command]
Expand Down Expand Up @@ -141,6 +149,14 @@ pub async fn finalize_rollback(

#[tauri::command]
pub async fn flake_installed_apps(app: AppHandle) -> Result<Vec<serde_json::Value>, String> {
if e2e_mock_system_enabled() {
log::info!(
"[apply] NIXMAC_E2E_MOCK_SYSTEM enabled; returning empty installed apps fixture"
);
// No current Product Proof surface depends on installed-app shape; avoid real Nix here.
return Ok(Vec::new());
}

let dir = store::ensure_config_dir_exists(&app)
.map_err(|e| capture_err("flake_installed_apps", e))?;

Expand All @@ -160,8 +176,18 @@ pub async fn flake_installed_apps(app: AppHandle) -> Result<Vec<serde_json::Valu
Ok(apps)
}

#[tauri::command]
pub async fn nix_check() -> Result<shared_types::NixCheckResult, String> {
fn nix_check_result() -> shared_types::NixCheckResult {
if e2e_mock_system_enabled() {
log::info!(
"[apply] NIXMAC_E2E_MOCK_SYSTEM enabled; reporting mocked Nix/darwin-rebuild availability"
);
return shared_types::NixCheckResult {
installed: true,
version: Some("NIXMAC_E2E_MOCK_SYSTEM mocked nix".to_string()),
darwin_rebuild_available: true,
};
}

let installed = nix::is_nix_installed();
let version = if installed {
nix::get_nix_version()
Expand All @@ -173,11 +199,16 @@ pub async fn nix_check() -> Result<shared_types::NixCheckResult, String> {
} else {
false
};
Ok(shared_types::NixCheckResult {
shared_types::NixCheckResult {
installed,
version,
darwin_rebuild_available,
})
}
}

#[tauri::command]
pub async fn nix_check() -> Result<shared_types::NixCheckResult, String> {
Ok(nix_check_result())
}

#[tauri::command]
Expand All @@ -202,8 +233,54 @@ pub async fn finalize_flake_lock(app: AppHandle) -> Result<shared_types::OkResul
/// Lists all darwinConfigurations defined in the flake.
#[tauri::command]
pub async fn flake_list_hosts(app: AppHandle) -> Result<Vec<String>, String> {
if e2e_mock_system_enabled() {
let host = e2e_mock_host_attr();
log::info!(
"[apply] NIXMAC_E2E_MOCK_SYSTEM enabled; returning mocked flake host {}",
host
);
return Ok(vec![host]);
}

let dir =
store::ensure_config_dir_exists(&app).map_err(|e| capture_err("flake_list_hosts", e))?;
let hosts = nix::list_darwin_hosts(&dir).map_err(|e| capture_err("flake_list_hosts", e))?;
Ok(hosts)
}

#[cfg(test)]
mod tests {
use super::*;

#[cfg(debug_assertions)]
#[test]
fn e2e_mock_nix_check_reports_available_system() {
let _env_lock = crate::test_support::e2e_env_lock();
let _env_restore = crate::test_support::EnvVarRestore::capture(&["NIXMAC_E2E_MOCK_SYSTEM"]);

std::env::set_var("NIXMAC_E2E_MOCK_SYSTEM", "1");
let result = nix_check_result();

assert!(result.installed);
assert!(result.darwin_rebuild_available);
assert_eq!(
result.version.as_deref(),
Some("NIXMAC_E2E_MOCK_SYSTEM mocked nix")
);
}

#[test]
fn e2e_mock_host_attr_uses_runtime_value_or_default() {
let _env_lock = crate::test_support::e2e_env_lock();
let _env_restore =
crate::test_support::EnvVarRestore::capture(&["HOME", "NIXMAC_E2E_HOST_ATTR"]);
let temp_home = tempfile::tempdir().unwrap();

std::env::set_var("HOME", temp_home.path());
std::env::remove_var("NIXMAC_E2E_HOST_ATTR");
assert_eq!(e2e_mock_host_attr(), "e2e-host");

std::env::set_var("NIXMAC_E2E_HOST_ATTR", "ci-host");
assert_eq!(e2e_mock_host_attr(), "ci-host");
}
}
12 changes: 11 additions & 1 deletion apps/native/src-tauri/src/commands/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ fn clean_field(value: &str, max_len: usize) -> String {
.to_string()
}

#[cfg(debug_assertions)]
fn e2e_breadcrumb_detail_limit(label: &str) -> usize {
if label.starts_with("native webview boot probe ") || label.starts_with("e2e-asset-fetch-") {
12_000
} else {
1_000
}
}

/// Records frontend boot breadcrumbs for E2E diagnostics.
///
/// This is debug-only and writes only when the E2E runtime file/env provides a
Expand Down Expand Up @@ -164,8 +173,9 @@ fn write_e2e_breadcrumb(
if label.is_empty() {
return Ok(());
}
let detail_limit = e2e_breadcrumb_detail_limit(&label);
let detail = detail
.map(|value| clean_field(value, 1_000))
.map(|value| clean_field(value, detail_limit))
.filter(|value| !value.is_empty());

std::fs::create_dir_all(diagnostics_dir)
Expand Down
10 changes: 10 additions & 0 deletions apps/native/src-tauri/src/e2e_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@
//! debug builds can still receive deterministic test controls. Release builds
//! ignore the file.

#[cfg(debug_assertions)]
use serde::Deserialize;
#[cfg(debug_assertions)]
use std::collections::HashMap;
#[cfg(debug_assertions)]
use std::fs;
#[cfg(debug_assertions)]
use std::path::PathBuf;
#[cfg(debug_assertions)]
use std::time::{SystemTime, UNIX_EPOCH};

#[cfg(debug_assertions)]
const RUNTIME_FILE_NAME: &str = "e2e-runtime.json";
#[cfg(debug_assertions)]
const BUNDLE_ID: &str = "com.darkmatter.nixmac";

#[cfg(debug_assertions)]
#[derive(Debug, Deserialize)]
struct E2eRuntimeFile {
#[serde(rename = "schemaVersion")]
Expand All @@ -26,6 +34,7 @@ struct E2eRuntimeFile {
values: HashMap<String, String>,
}

#[cfg(debug_assertions)]
fn runtime_file_path() -> Option<PathBuf> {
let home = dirs::home_dir()?;
Some(
Expand All @@ -36,6 +45,7 @@ fn runtime_file_path() -> Option<PathBuf> {
)
}

#[cfg(debug_assertions)]
fn now_unix() -> Option<u64> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
Expand Down
19 changes: 11 additions & 8 deletions apps/native/src-tauri/src/evolve/search_packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ fn process_search_output(

if let Some(value) = parsed.as_object() {
for (attr_path, pkg) in value {
let name = attr_path.split('.').last().unwrap_or(attr_path).to_string();
let name = attr_path
.split('.')
.next_back()
.unwrap_or(attr_path)
.to_string();
let package_type = if let Some(classifier) = package_classifier {
classifier(&name)
} else {
Expand Down Expand Up @@ -138,7 +142,10 @@ fn process_channel_results(
.split('.')
.next_back()
.unwrap_or(&result.attr_path);
if !structured.iter().any(|item| item.attr_path == result.attr_path) {
if !structured
.iter()
.any(|item| item.attr_path == result.attr_path)
{
structured.push(result);
}
}
Expand Down Expand Up @@ -242,11 +249,7 @@ fn classify_derivation(drv: &str) -> SearchResultInstallTarget {
/// (Homebrew Cask-like) or a CLI / nix-native package.
fn classify_package(channel: &str, attr_path: &str) -> SearchResultInstallTarget {
let mut cmd = Command::new("nix");
cmd.args(&[
"derivation",
"show",
&format!("{}#{}", channel, attr_path),
]);
cmd.args(["derivation", "show", &format!("{}#{}", channel, attr_path)]);

let output = match cmd.output() {
Ok(output) => output,
Expand Down Expand Up @@ -335,7 +338,7 @@ mod tests {
channel: "test-channel".to_string(),
version: "13.2".to_string(),
description: "Extensible package for writing and formatting TeX files in GNU Emacs and XEmacs".to_string(),
install_via: SearchResultInstallTarget::Either,
install_via: SearchResultInstallTarget::Either,
})), ("empty", 0, None )];
let fake_package_classifier = |_package_name: &str| SearchResultInstallTarget::Either;

Expand Down
Loading
Loading