Skip to content
Merged
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
18 changes: 0 additions & 18 deletions sanitizing.allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,6 @@
"reason": "Came out of the split of dispatch_pty_input_operation_core in #3180 (138d9f0). Complexity is not a blocker for this function.",
"issue": 3182,
"since": "2026-10-23"
},
{
"gate": "09",
"rule": "launch_files",
"path": "src-tauri/src/config/agency_cache.rs",
"symbol": "process_is_running",
"reason": "moved unchanged from cli/agency_templates (03-R, batch 6)",
"issue": 3195,
"since": "2026-10-24"
},
{
"gate": "09",
"rule": "launch_files",
"path": "src-tauri/src/config/agency_cache/git.rs",
"symbol": "run_process_with_timeout",
"reason": "moved unchanged from cli/agency_templates (03-R, batch 6)",
"issue": 3195,
"since": "2026-10-24"
}
]
}
17 changes: 16 additions & 1 deletion scripts/sanitizing_tauri_edge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,29 @@ function signals(scan, item) {
return item.attributes.flatMap(a => a.signals.map(meta => ({ ...resolveAlias(scan,meta.name,a.scope), meta, line: a.line })));
}
export function runGate(snapshot) {
return runMeasuredGate(snapshot, { gate, rules, newFileRules, inUniverse, measure, extraChecks, recognizeMoves: true });
const measureFile = (path, scan) => {
const m = measure(path, scan);
// Production debt stays comparable. New core files must also be Tauri-free
// in test-only code, using the same alias-aware analysis as production.
if (!snapshot.baseTree.has(path)) {
const testTokens = scan.tokens.filter(t => scan.isTestOffset(t.start));
m.testTauri = measure(path, { ...scan, productionTokens: testTokens }).sites.tauri_files;
}
return m;
};
return runMeasuredGate(snapshot, {
gate, rules, newFileRules, inUniverse, measure: measureFile, extraChecks, recognizeMoves: true,
});
}

const gate = '06';
const rules = ['tauri_files', 'emit_files', 'state_files', 'command_files', 'disk_commands'];
const newFileRules = ['tauri_files'];
const inUniverse = p => /^src-tauri\/src\/.*\.rs$/.test(p);
function extraChecks(path, m, before, violation, moved) {
if (!before && m.testTauri?.length)
violation('tauri_files', path, m.testTauri[0].line, 0, m.testTauri.length,
'new core file must be Tauri-free, including test-only code');
for (const [id, site] of m.commands) if (!before?.commandIds.has(id) && !moved.has(site))
violation('new_disk_command', path, site.line, 0, 1, `new command with disk access: ${id}`);
}
Expand Down
37 changes: 33 additions & 4 deletions scripts/sanitizing_tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,26 @@ test('05 debt equal/improved/worse and strict compensated new file, C8 ratchet o
// Visibility has a global ratchet, not an invented strict new-file prohibition.
assert.equal(layoutGate(trees({[p]:'pub mod a;'},{[q]:'pub mod a;'})).exit,0);
});
test('06 new core nodes reject Tauri directly, by alias and under cfg(test)', () => {
for (const name of ['repos.rs', 'another_core.rs']) {
const p = rustRoot + name;
const clean = 'fn name() {}';
assert.equal(edgeGate(trees({}, {[p]: clean})).exit, 0);
for (const code of [
'fn f(x: tauri::AppHandle) {}',
'use tauri::AppHandle as Handle; fn f(x: Handle) {}',
'#[cfg(test)] fn f(x: tauri::AppHandle) {}',
'#![cfg(test)] use tauri as ui; fn f(x: ui::AppHandle) {}',
'#[cfg(test)] mod tests { use tauri::AppHandle as Handle; fn f(x: Handle) {} }',
]) {
const result = edgeGate(trees({}, {[p]: code}));
assert.equal(result.exit, 1, name + ': ' + code);
assert.ok(result.diagnostics.some(d => d.status === 'FAIL' &&
d.rule === 'tauri_files' && d.path === p && d.line > 0 && d.remedy));
}
assert.equal(edgeGate(trees({}, {[p]: clean})).exit, 0);
}
});
test('06 every counter and exact boundary, alias imports and homonymous methods', () => {
for(const [rule,bad] of [
['tauri_files','fn f(x: tauri::AppHandle) {}'],
Expand Down Expand Up @@ -553,25 +573,34 @@ test('09 independent counters, absolute write, exact ownership and aliases/shado
const shadow='use std::env::var as read; fn f(read: F) { read("x"); }';
assert.equal(processGate(trees({}, {[rustRoot+'x.rs']:shadow})).exit,0);
});
test('P02 production predicates preserve following code; comments/strings and tests do not signal', () => {
test('P02 production predicates preserve following code; comments/strings and tests add no production debt', () => {
const assertTestOnly = (gate, rule, path, code) => {
const base = {[path]: code};
assert.equal(gate(trees(base)).exit, 0);
const added = gate(trees({}, base));
assert.equal(added.totals[1][rule] ?? 0, 0);
// The new-file Tauri rule includes tests; production counters still exclude them.
assert.equal(added.exit, gate === edgeGate ? 1 : 0);
};
for(const [gate,bad,rule] of [[edgeGate,'tauri::AppHandle::new();','tauri_files'],
[globalsGate,'static X: Mutex<X> = init();','globals'],[processGate,'env::var("x");','env_read_files']]) {
for(const cfg of ['test','all(test,unix)','any(test,test)']) {
const s=`#[cfg(${cfg})] fn test_only() { ${bad} }`;
assert.equal(gate(trees({}, {[rustRoot+'x.rs']:s})).exit,0);
assertTestOnly(gate,rule,rustRoot+'x.rs',s);
control(gate,rule,'x.rs',s,s+` fn production() { ${bad} }`);
}
for(const cfg of ['any(has_embedded_dist,test)','not(test)','all()','any()'])
control(gate,rule,'x.rs','',`#[cfg(${cfg})] fn production() { ${bad} }`);
control(gate,rule,'x.rs','',`#[cfg_attr(unix,cfg(test))] fn production() { ${bad} }`);
for(const attr of ['#[test]','#[tokio::test]','#[async_std::test]','use tokio::test as check; #[check]']) {
const s=`${attr} async fn test_fn() { ${bad} }`;
assert.equal(gate(trees({}, {[rustRoot+'x.rs']:s})).exit,0);
assertTestOnly(gate,rule,rustRoot+'x.rs',s);
control(gate,rule,'x.rs',s,s+` fn production() { ${bad} }`);
}
const inert='// '+bad+'\nfn f() { let x = r###"'+bad+'"###; }';
assert.equal(gate(trees({}, {[rustRoot+'x.rs']:inert})).exit,0);
for(const p of ['tests.rs','x_tests.rs','tests/a.rs']) assert.equal(gate(trees({}, {[rustRoot+p]:`fn f() { ${bad} }`})).exit,0);
for(const p of ['tests.rs','x_tests.rs','tests/a.rs'])
assertTestOnly(gate,rule,rustRoot+p,`fn f() { ${bad} }`);
}
});
test('09 moved occurrences pass; compensated new code and copies fail', () => {
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/module-arcs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,7 @@ agentscommander_lib::commands::quit_gate -> agentscommander_lib::quit_gate
agentscommander_lib::commands::repos -> agentscommander_lib::config::agent_path
agentscommander_lib::commands::repos -> agentscommander_lib::config::settings
agentscommander_lib::commands::repos -> agentscommander_lib::pty::credentials
agentscommander_lib::commands::repos -> agentscommander_lib::repos
agentscommander_lib::commands::resource_monitor -> agentscommander_lib
agentscommander_lib::commands::resource_monitor -> agentscommander_lib::config::settings
agentscommander_lib::commands::resource_monitor -> agentscommander_lib::pty::manager
Expand Down Expand Up @@ -1412,6 +1413,7 @@ agentscommander_lib::config::session_context -> agentscommander_lib::config::sha
agentscommander_lib::config::session_context -> agentscommander_lib::phone::messaging
agentscommander_lib::config::session_context -> agentscommander_lib::pty::container_repos
agentscommander_lib::config::session_context -> agentscommander_lib::pty::credentials
agentscommander_lib::config::session_context -> agentscommander_lib::repos
agentscommander_lib::config::session_context::agent_template -> agentscommander_lib::config::root_agent
agentscommander_lib::config::session_context::agent_template -> agentscommander_lib::config::seed_manifest
agentscommander_lib::config::session_context::agent_template -> agentscommander_lib::config::session_context
Expand Down
47 changes: 2 additions & 45 deletions src-tauri/src/commands/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::path::Path;
use tauri::State;

use crate::config::settings::SettingsState;
pub(crate) use crate::repos::derive_repo_name;
use crate::repos::strip_userinfo;

/// Agent detection: label + list of possible markers (files or dirs)
/// An agent is detected if ANY of its markers exist in the repo.
Expand Down Expand Up @@ -31,21 +33,6 @@ fn detect_agents(repo_path: &Path) -> Vec<String> {
.collect()
}

/// Derive extended repo name as "parent/repo" from an absolute path.
/// Always uses forward slash as separator regardless of OS.
pub fn derive_repo_name(path: &Path) -> Option<String> {
let file_name = path.file_name().and_then(|n| n.to_str())?;
let name = match path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
{
Some(parent) => format!("{}/{}", parent, file_name),
None => file_name.to_string(),
};
Some(name)
}

/// Add a repo to results if it matches the query and hasn't been seen yet
pub fn try_add_repo(
path: &Path,
Expand Down Expand Up @@ -148,36 +135,6 @@ pub async fn search_repos(
/// The context menu must never wait on a stalled git.
const GIT_REMOTE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);

/// #943 - drop `user[:password]@` from a scheme-form remote URL.
///
/// `git remote get-url` returns whatever `.git/config` holds, and a repo cloned
/// with a token carries `https://<user>:<ghp_...>@github.com/o/r.git`. The caller
/// only needs host + owner + repo, so the secret is destroyed here and never
/// crosses the IPC boundary into the webview (nor into devtools, nor into any
/// future console log). scp-like remotes (`git@github.com:o/r.git`) are returned
/// untouched: that `git` is an ssh user name, not a credential, and the frontend
/// parser ignores it.
fn strip_userinfo(remote: &str) -> String {
let Some(scheme_end) = remote.find("://") else {
return remote.to_string();
};
let authority_start = scheme_end + 3;
let authority_end = remote[authority_start..]
.find('/')
.map(|i| authority_start + i)
.unwrap_or(remote.len());
let authority = &remote[authority_start..authority_end];
match authority.rfind('@') {
Some(at) => format!(
"{}{}{}",
&remote[..authority_start],
&authority[at + 1..],
&remote[authority_end..]
),
None => remote.to_string(),
}
}

/// #943 - `origin`'s URL for a repo working dir, userinfo stripped, or `None`.
///
/// Every failure mode (path missing, not a work tree root, no `origin`, git not on
Expand Down
34 changes: 0 additions & 34 deletions src-tauri/src/commands/repos/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,6 @@ fn git_in(dir: &Path, args: &[&str]) -> bool {

const ORIGIN: &str = "https://github.com/mblua/AgentsCommander.git";

#[test]
fn strip_userinfo_removes_credentials_from_scheme_urls() {
assert_eq!(
strip_userinfo("https://u:tok@github.com/o/r.git"),
"https://github.com/o/r.git"
);
assert_eq!(
strip_userinfo("ssh://git@github.com/o/r.git"),
"ssh://github.com/o/r.git"
);
}

#[test]
fn strip_userinfo_leaves_everything_else_alone() {
// No userinfo.
assert_eq!(
strip_userinfo("https://github.com/o/r.git"),
"https://github.com/o/r.git"
);
// scp-like: no `://`, and the `git@` is an ssh user name, not a secret.
assert_eq!(
strip_userinfo("git@github.com:o/r.git"),
"git@github.com:o/r.git"
);
// `@` in the path, none in the authority: must not be treated as userinfo.
assert_eq!(
strip_userinfo("https://github.com/o/r@2.git"),
"https://github.com/o/r@2.git"
);
// Degenerate inputs must not panic.
assert_eq!(strip_userinfo(""), "");
assert_eq!(strip_userinfo("https://"), "https://");
}

/// T1 - a path that does not exist yields None, and never spawns git: the
/// `.git` stat fails first.
#[tokio::test]
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub(crate) mod path_utils;
pub mod phone;
pub(crate) mod process;
pub mod pty;
pub(crate) mod repos;
pub mod resource_monitor;
pub mod screenshot;
pub mod session;
Expand Down
49 changes: 49 additions & 0 deletions src-tauri/src/repos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::path::Path;

/// Derive extended repo name as "parent/repo" from an absolute path.
/// Always uses forward slash as separator regardless of OS.
pub(crate) fn derive_repo_name(path: &Path) -> Option<String> {
let file_name = path.file_name().and_then(|n| n.to_str())?;
let name = match path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
{
Some(parent) => format!("{}/{}", parent, file_name),
None => file_name.to_string(),
};
Some(name)
}

/// #943 - drop `user[:password]@` from a scheme-form remote URL.
///
/// `git remote get-url` returns whatever `.git/config` holds, and a repo cloned
/// with a token carries `https://<user>:<ghp_...>@github.com/o/r.git`. The caller
/// only needs host + owner + repo, so the secret is destroyed here and never
/// crosses the IPC boundary into the webview (nor into devtools, nor into any
/// future console log). scp-like remotes (`git@github.com:o/r.git`) are returned
/// untouched: that `git` is an ssh user name, not a credential, and the frontend
/// parser ignores it.
pub(crate) fn strip_userinfo(remote: &str) -> String {
let Some(scheme_end) = remote.find("://") else {
return remote.to_string();
};
let authority_start = scheme_end + 3;
let authority_end = remote[authority_start..]
.find('/')
.map(|i| authority_start + i)
.unwrap_or(remote.len());
let authority = &remote[authority_start..authority_end];
match authority.rfind('@') {
Some(at) => format!(
"{}{}{}",
&remote[..authority_start],
&authority[at + 1..],
&remote[authority_end..]
),
None => remote.to_string(),
}
}

#[cfg(test)]
mod tests;
48 changes: 48 additions & 0 deletions src-tauri/src/repos/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use super::*;

#[test]
fn strip_userinfo_removes_credentials_from_scheme_urls() {
assert_eq!(
strip_userinfo("https://u:tok@github.com/o/r.git"),
"https://github.com/o/r.git"
);
assert_eq!(
strip_userinfo("ssh://git@github.com/o/r.git"),
"ssh://github.com/o/r.git"
);
}

#[test]
fn strip_userinfo_leaves_everything_else_alone() {
// No userinfo.
assert_eq!(
strip_userinfo("https://github.com/o/r.git"),
"https://github.com/o/r.git"
);
// scp-like: no `://`, and the `git@` is an ssh user name, not a secret.
assert_eq!(
strip_userinfo("git@github.com:o/r.git"),
"git@github.com:o/r.git"
);
// `@` in the path, none in the authority: must not be treated as userinfo.
assert_eq!(
strip_userinfo("https://github.com/o/r@2.git"),
"https://github.com/o/r@2.git"
);
// Degenerate inputs must not panic.
assert_eq!(strip_userinfo(""), "");
assert_eq!(strip_userinfo("https://"), "https://");
}

#[test]
fn derive_repo_name_preserves_path_names() {
assert_eq!(derive_repo_name(Path::new("/")), None);
assert_eq!(
derive_repo_name(Path::new("parent/repo")),
Some("parent/repo".into())
);
assert_eq!(
derive_repo_name(Path::new("padre/日本語")),
Some("padre/日本語".into())
);
}
2 changes: 1 addition & 1 deletion src-tauri/tests/c4_state_key_census.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ fn assert_frozen(id: &str, hits: &[Hit], pin: &Pin) {
#[test]
fn the_inventory_is_the_certified_file_set() {
let sources = rust_sources();
assert_eq!(sources.len(), 1281, "inventory count");
assert_eq!(sources.len(), 1283, "inventory count");
for (path, text) in &sources {
assert!(path.starts_with("src-tauri/src/"), "path form: {path}");
assert!(!path.contains('\\'), "path separator: {path}");
Expand Down
Loading