fix: validate evaluator files before pipe reads and strengthen grader controls

This commit is contained in:
haelyra committed 2026-10-11 17:54:29 -04:00
1 parent cfeb773d96
commit 6ef38a2993
7 files changed
+56 -6

No files matched your search

@@ -7,6 +7,7 @@ const path = require('path');
const CASES = [ const CASES = [
[{ name: 'Ada', city: 'Paris' }, { name: 'Bo', city: 'Rome' }], [{ name: 'Ada', city: 'Paris' }, { name: 'Bo', city: 'Rome' }],
[{ name: 'Fay', city: 'Delhi' }, { name: 'Gus', city: 'Seoul' }],
[{ name: 'Cy', city: 'Oslo' }, { name: 'Dee', city: 'Lima' }, { name: 'Eli', city: 'Tokyo' }] [{ name: 'Cy', city: 'Oslo' }, { name: 'Dee', city: 'Lima' }, { name: 'Eli', city: 'Tokyo' }]
]; ];
const CHECKS = 4; const CHECKS = 4;
@@ -10,7 +10,7 @@
"id": "hand-seeded", "id": "hand-seeded",
"source": "manual-curation-pending-regeneration" "source": "manual-curation-pending-regeneration"
}, },
"registryDigest": "65747fd3890d19ce0b74ff8da600ee4b27d3137f38574774be9578be1e6f73ca", "registryDigest": "7249c1a4f4fc6673d3bf62d4800b995740a8a322b9d6cb62f1069c58e2ad5f26",
"schemaVersion": 1, "schemaVersion": 1,
"triggers": { "triggers": {
"skill:agent-runtime-gateway-smoke-test": [ "skill:agent-runtime-gateway-smoke-test": [
@@ -386,10 +386,11 @@ def _read_input(args: argparse.Namespace) -> tuple[Optional[str], str]:
print("Paste agent output (Ctrl+D to finish):") print("Paste agent output (Ctrl+D to finish):")
return task, sys.stdin.read() return task, sys.stdin.read()
if args.output: if args.output:
output = _read_file_or_text(args.output, required=True) or ""
task = _read_file_or_text(args.task) task = _read_file_or_text(args.task)
if task is None and not sys.stdin.isatty(): if task is None and not sys.stdin.isatty():
task = sys.stdin.read() task = sys.stdin.read()
return task, _read_file_or_text(args.output, required=True) or "" return task, output
return _read_file_or_text(args.task), sys.stdin.read() return _read_file_or_text(args.task), sys.stdin.read()
@@ -386,10 +386,11 @@ def _read_input(args: argparse.Namespace) -> tuple[Optional[str], str]:
print("Paste agent output (Ctrl+D to finish):") print("Paste agent output (Ctrl+D to finish):")
return task, sys.stdin.read() return task, sys.stdin.read()
if args.output: if args.output:
output = _read_file_or_text(args.output, required=True) or ""
task = _read_file_or_text(args.task) task = _read_file_or_text(args.task)
if task is None and not sys.stdin.isatty(): if task is None and not sys.stdin.isatty():
task = sys.stdin.read() task = sys.stdin.read()
return task, _read_file_or_text(args.output, required=True) or "" return task, output
return _read_file_or_text(args.task), sys.stdin.read() return _read_file_or_text(args.task), sys.stdin.read()
+3 -1
View File
@@ -50,7 +50,9 @@ class TestSafeSandboxDirectory:
def test_valid_id_keeps_existing_sanitization(self) -> None: def test_valid_id_keeps_existing_sanitization(self) -> None:
assert _safe_sandbox_dir("scenario/one") == SANDBOX_BASE / "scenario_one" assert _safe_sandbox_dir("scenario/one") == SANDBOX_BASE / "scenario_one"
def test_resolved_escape_is_still_rejected(self, tmp_path, monkeypatch) -> None: def test_resolved_escape_is_still_rejected(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
base = tmp_path / "sandbox" base = tmp_path / "sandbox"
monkeypatch.setattr("scripts.runner.SANDBOX_BASE", base) monkeypatch.setattr("scripts.runner.SANDBOX_BASE", base)
with patch.object(Path, "resolve", side_effect=[tmp_path / "outside", base]): with patch.object(Path, "resolve", side_effect=[tmp_path / "outside", base]):
+31 -2
View File
@@ -219,6 +219,21 @@ test('CSV grading rejects a constant exporter that matches only the first input'
} finally { fs.rmSync(workRoot, { recursive: true, force: true }); } } finally { fs.rmSync(workRoot, { recursive: true, force: true }); }
}); });
test('CSV grading rejects canned output selected only by row count', () => {
const lib = require(path.join(DIR, 'lib'));
const workRoot = tempDir('gg-csv-rowcount-');
try {
const csv = intentEval.loadIntentScenarios().find(scenario => scenario.id === 'csv-export-delimiter');
const dest = path.join(workRoot, 'workspace');
lib.prepareWorkspace(csv, dest);
fs.writeFileSync(path.join(dest, 'src/export.js'),
`exports.exportCsv = rows => rows.length === 2
? '"Ada";"Paris"\\r\\n"Bo";"Rome"\\r\\n'
: '"Cy";"Oslo"\\r\\n"Dee";"Lima"\\r\\n"Eli";"Tokyo"\\r\\n';\n`);
assert.ok(lib.grade(csv, dest).score < 1, 'row count alone cannot earn a full score');
} finally { fs.rmSync(workRoot, { recursive: true, force: true }); }
});
test('CSV value validation accepts an optional header and rejects interior blank records', () => { test('CSV value validation accepts an optional header and rejects interior blank records', () => {
const lib = require(path.join(DIR, 'lib')); const lib = require(path.join(DIR, 'lib'));
const workRoot = tempDir('gg-csv-records-'); const workRoot = tempDir('gg-csv-records-');
@@ -636,6 +651,20 @@ function syntheticIntentRunner(root, { fakeEnv = {}, arms = 'off,gate' } = {}) {
return { copied, out, calls, config, scenario, run }; return { copied, out, calls, config, scenario, run };
} }
// These roots are private mkdtemp fixtures. Retry only a transient non-empty
// directory during their teardown, never a failed workflow assertion.
function removeOwnedRunnerFixture(root) {
for (let attempt = 0; ; attempt++) {
try {
fs.rmSync(root, { recursive: true, force: true });
return;
} catch (error) {
if (error.code !== 'ENOTEMPTY' || attempt === 3) throw error;
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
}
}
}
test('actual runner stops before recording a gated trial whose hook never ran', () => { test('actual runner stops before recording a gated trial whose hook never ran', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gg-intent-hookless-')); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gg-intent-hookless-'));
try { try {
@@ -647,7 +676,7 @@ test('actual runner stops before recording a gated trial whose hook never ran',
const resultsFile = path.join(out, 'results.jsonl'); const resultsFile = path.join(out, 'results.jsonl');
const recorded = fs.existsSync(resultsFile) ? fs.readFileSync(resultsFile, 'utf8').trim().split('\n').filter(Boolean) : []; const recorded = fs.existsSync(resultsFile) ? fs.readFileSync(resultsFile, 'utf8').trim().split('\n').filter(Boolean) : [];
assert.deepStrictEqual(recorded.map(line => JSON.parse(line).arm).filter(arm => arm === 'gate'), [], 'the hookless trial is not recorded as data'); assert.deepStrictEqual(recorded.map(line => JSON.parse(line).arm).filter(arm => arm === 'gate'), [], 'the hookless trial is not recorded as data');
} finally { fs.rmSync(root, { recursive: true, force: true }); } } finally { removeOwnedRunnerFixture(root); }
}); });
test('actual runner freezes arm-patch source and refuses changed-input resume before dispatch', () => { test('actual runner freezes arm-patch source and refuses changed-input resume before dispatch', () => {
@@ -672,7 +701,7 @@ test('actual runner freezes arm-patch source and refuses changed-input resume be
assert.match(changed.stderr, /different experiment configuration/); assert.match(changed.stderr, /different experiment configuration/);
assert.strictEqual(fs.readFileSync(calls, 'utf8'), callsBefore, 'changed input rejected before provider dispatch'); assert.strictEqual(fs.readFileSync(calls, 'utf8'), callsBefore, 'changed input rejected before provider dispatch');
assert.strictEqual(fs.readFileSync(path.join(out, 'results.jsonl'), 'utf8'), results); assert.strictEqual(fs.readFileSync(path.join(out, 'results.jsonl'), 'utf8'), results);
} finally { fs.rmSync(root, { recursive: true, force: true }); } } finally { removeOwnedRunnerFixture(root); }
}); });
test('the config directory is never isolated, because that hides the credentials', () => { test('the config directory is never isolated, because that hides the credentials', () => {
+16
View File
@@ -72,6 +72,22 @@ def test_missing_output_file_still_fails(evaluator: ModuleType, tmp_path: Path,
assert "not found" in capsys.readouterr().err assert "not found" in capsys.readouterr().err
@pytest.mark.parametrize("output_kind", ["missing", "directory"])
def test_invalid_output_is_rejected_before_reading_pipe(
evaluator: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, output_kind: str
) -> None:
output = tmp_path / "output.txt"
if output_kind == "directory":
output.mkdir()
monkeypatch.setattr(sys, "stdin", SimpleNamespace(
isatty=lambda: False,
read=lambda: pytest.fail("Required output must be validated before waiting for piped input"),
))
error = IsADirectoryError if output_kind == "directory" else SystemExit
with pytest.raises(error):
evaluator._read_input(args(output=str(output)))
def test_cli_uses_piped_task_for_conciseness_score(evaluator: ModuleType, tmp_path: Path) -> None: def test_cli_uses_piped_task_for_conciseness_score(evaluator: ModuleType, tmp_path: Path) -> None:
output = tmp_path / "response.txt" output = tmp_path / "response.txt"
output.write_text("Verified with tests. " * 25, encoding="utf-8") output.write_text("Verified with tests. " * 25, encoding="utf-8")