Merge late GateGuard filter and security fixture repairs

This commit is contained in:
haelyra
2026-10-11 03:54:05 -04:00
6 changed files with 74 additions and 11 deletions
+19 -6
View File
@@ -377,10 +377,10 @@ function grepToolFilters(glob, type) {
const filters = { positives: [], includes: [], exclusions: [], opaque: false };
if (typeof type === 'string' && type) {
const globs = rgTypeGlobs(type);
if (globs) filters.includes.push(...globs);
if (globs) filters.typeIncludes = globs;
else filters.opaque = true;
}
if (typeof glob !== 'string' || !glob) return filters;
if (typeof glob !== 'string' || !glob) return resolveTypeFilters(filters);
for (const part of splitTopLevelCommas(glob)) {
if (part.startsWith('!')) filters.exclusions.push(part.slice(1));
else {
@@ -388,7 +388,18 @@ function grepToolFilters(glob, type) {
filters.includes.push(part);
}
}
return filters;
return resolveTypeFilters(filters);
}
function resolveTypeFilters({ typeIncludes = [], typeExclusions = [], ...filters }) {
// rg's explicit positive globs override type selection, not union with it.
// Keep type alternatives separate until every explicit glob is known.
const hasIncludes = filters.includes.length > 0;
return {
...filters,
includes: hasIncludes ? filters.includes : typeIncludes,
exclusions: filters.exclusions.concat(hasIncludes ? [] : typeExclusions)
};
}
function shellSearchFilters(kind, args) {
@@ -401,7 +412,7 @@ function shellSearchFilters(kind, args) {
const consumed = applyShellFilterArg(kind, args, i, filters);
i += consumed;
}
return filters;
return resolveTypeFilters(filters);
}
function applyShellFilterArg(kind, args, i, filters) {
@@ -418,8 +429,10 @@ function applyShellFilterArg(kind, args, i, filters) {
const type = (name, indexes, excluded) => {
const globs = rgTypeGlobs(name);
if (!globs) filters.opaque = true;
else if (excluded) globs.forEach(glob => exclude(glob, indexes));
else globs.forEach(include);
else if (excluded) {
filters.typeExclusions = (filters.typeExclusions || []).concat(globs);
indexes.forEach(index => filters.dropped.add(index));
} else filters.typeIncludes = (filters.typeIncludes || []).concat(globs);
};
const extension = value => include(value ? `*.${String(value).replace(/^\./, '')}` : '');
if (kind.startsWith('git ') && GIT_EXCLUDE_PATHSPEC.test(arg)) {
@@ -0,0 +1,25 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
if (process.platform === 'win32') {
console.log('Skipping non-Windows Python availability control.');
} else {
const emptyPath = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-missing-python-'));
try {
const result = spawnSync(process.execPath, [path.join(__dirname, 'observe-secret-redaction.test.js')], {
env: { ...process.env, PATH: emptyPath }, encoding: 'utf8', timeout: 15000
});
assert.equal(result.status, 1, result.stderr);
assert.match(result.stdout, /FAIL python3 is required/);
assert.match(result.stdout, /Passed: 0\nFailed: 1/);
assert.doesNotMatch(result.stdout, /Skipping shell fixtures/);
console.log('PASS missing Python fails the actual non-Windows redaction gate');
} finally {
fs.rmSync(emptyPath, { recursive: true, force: true });
}
}
+7 -3
View File
@@ -41,7 +41,13 @@ function capture(phase, input) {
}
console.log('=== Actual registered observation secret redaction ===');
if (process.platform !== 'win32' && spawnSync('python3', ['--version']).status === 0) {
if (process.platform === 'win32') {
console.log('Skipping shell fixtures: native Windows runner requires platform CI.');
} else if (spawnSync('python3', ['--version']).status !== 0) {
console.log(' FAIL python3 is required for observation redaction tests');
failed += 1;
} else {
// Synthetic canary data proves that captured credential-shaped text is masked.
for (const [label, content] of [
['double-quoted source', 'const apiKey = "CANARY_TOKEN_abcdefghijk";'],
['single-quoted source', "const password = 'CANARY_TOKEN_abcdefghijk';"],
@@ -61,8 +67,6 @@ if (process.platform !== 'win32' && spawnSync('python3', ['--version']).status =
assert.ok(records.includes('[REDACTED]'));
});
}
} else {
console.log('Skipping shell fixtures: native Windows runner requires platform CI.');
}
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
+4 -2
View File
@@ -163,8 +163,9 @@ test('quoted staged paths and untracked paths share identity, and untracked fail
assert.ok(snapshot.agents.every(agent => agent.files.includes(file)));
for (const code of ['EACCES', 'ETIMEDOUT', 'ENOBUFS']) {
childProcess.execFileSync = (command, args, options) => {
assert.strictEqual(command, 'git', 'fixture only forwards Git probes');
if (args.includes('ls-files')) throw Object.assign(new Error('untracked probe unavailable'), { code });
return originalExec(command, args, options);
return originalExec('git', args, { ...options, shell: false });
};
delete require.cache[modulePath];
const fresh = require(modulePath);
@@ -172,8 +173,9 @@ test('quoted staged paths and untracked paths share identity, and untracked fail
assert.ok(tracked, `${code}: tracked changes must survive`);
assert.deepStrictEqual(tracked.files.map(entry => entry.path).sort(), ['README', file].sort());
childProcess.execFileSync = (command, args, options) => {
assert.strictEqual(command, 'git', 'fixture only forwards Git probes');
if (args.includes('--name-only')) throw Object.assign(new Error('tracked names unavailable'), { code });
return originalExec(command, args, options);
return originalExec('git', args, { ...options, shell: false });
};
delete require.cache[modulePath];
const partial = require(modulePath).sessionsToAgents([worktrees[0]]);
@@ -209,6 +209,8 @@ test('a search confined to the target file name is a lookup, not a search', () =
['Grep', { pattern: 'tokenizer', glob: '**/tokenizer.js' }],
['Bash', { command: 'find src -name tokenizer.js' }],
['Bash', { command: 'rg -g tokenizer.js tokenizer src' }],
['Bash', { command: 'rg -t js -g tokenizer.js tokenizer src' }],
['Grep', { pattern: 'tokenizer', type: 'js', glob: 'tokenizer.js' }],
['Bash', { command: 'rg --files -g tokenizer.js src' }],
['Bash', { command: 'rg tokenizer src/**/tokenizer.js' }]
];
@@ -105,6 +105,23 @@ test('rg -T/--type-not excludes the type globs', () => {
}
});
test('explicit rg globs take precedence over type alternatives in either flag order', () => {
for (const args of [['-t', 'js', '-g', '*.py'], ['-g', '*.py', '--type=js']]) {
const item = rgItem([...args, 'tokenizer', 'src']);
assert.strictEqual(admitted(item, 'src/tokenizer.js'), false, args.join(' '));
assert.strictEqual(admitted(item, 'src/tokenizer.py'), true, args.join(' '));
}
assert.strictEqual(admitted(rgItem(['-t', 'py', '-g', '*.js']), 'src/tokenizer.js'), true);
assert.strictEqual(admitted(rgItem(['-T', 'js', '-g', '*.js']), 'src/tokenizer.js'), true);
assert.strictEqual(admitted(rgItem(['-t', 'js', '-g', '!*.js']), 'src/tokenizer.js'), false);
});
test('Grep tool explicit globs do not gain an alternative from its type', () => {
const item = { ...grepToolFilters('*.py', 'js'), scopes: [['.']], pathIncludes: true };
assert.strictEqual(admitted(item, 'src/tokenizer.js'), false);
assert.strictEqual(admitted(item, 'src/tokenizer.py'), true);
});
test('unknown or redefined rg types are unreadable filters', () => {
for (const args of [['-t', 'nosuchtype'], ['-T', 'nosuchtype'], ['--type-add', 'web:*.js', '-t', 'web'], ['--type-clear', 'js']]) {
assert.strictEqual(admitted(rgItem([...args, 'x']), 'src/tokenizer.js'), false, args.join(' '));