fix(ms-ai-architect): G6 Layer B — unicode/carrier-scan kjører pre-write via temp-fil (lukker ingestion-brief §8-akseptansegap, TDD) [skip-docs]

This commit is contained in:
Kjell Tore Guttormsen 2026-07-04 23:04:00 +02:00
commit b227c278eb
2 changed files with 77 additions and 4 deletions

View file

@ -23,7 +23,9 @@
// pattern as scripts/kb-eval/score-skill.mjs.
import { fileURLToPath } from 'node:url';
import { dirname, resolve, basename } from 'node:path';
import { dirname, resolve, basename, join } from 'node:path';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -80,7 +82,9 @@ const ENCODED_MIN_ENTROPY = 4.0;
* driven with a single-file discovery so no charset is re-derived here.
*
* @param {string} content the candidate file content
* @param {{path?: string}} [opts] path is required for unicode detection (the scanner reads it)
* @param {{path?: string}} [opts] optional on-disk path; when absent the unicode scan runs
* PRE-WRITE against a temp file materialized from `content` (the ingestion gate), so unicode
* coverage no longer depends on the file already existing on disk
* @returns {Promise<Array<{class: string, subtype?: string, severity: string, line: number, evidence: string}>>}
*/
export async function detectAdversarial(content, opts = {}) {
@ -108,8 +112,22 @@ export async function detectAdversarial(content, opts = {}) {
}
// --- unicode (disk-based scanner; needs a real path) ---
if (opts.path) {
const discovery = { files: [{ absPath: resolve(opts.path), relPath: basename(opts.path) }] };
// The llm-security unicode scanner reads a file off disk. When a caller passes content
// WITHOUT a path — the pre-write ingestion gate: scan the COMPOSED artefact before it is
// ever written — materialize a temp file so the SAME scanner runs pre-write. This closes
// the ingestion-brief §8 acceptance test (zero-width/bidi rejected BEFORE write); the old
// `if (opts.path)` guard skipped unicode entirely on the content path, leaving pre-write
// carriers undetected. Fail-closed posture is unchanged — a scanner error still propagates
// as a throw → the caller BLOCKs.
let scanPath = opts.path;
let tmpDir = null;
try {
if (!scanPath) {
tmpDir = mkdtempSync(join(tmpdir(), 'kb-adv-'));
scanPath = join(tmpDir, 'candidate.md');
writeFileSync(scanPath, String(content ?? ''), 'utf8');
}
const discovery = { files: [{ absPath: resolve(scanPath), relPath: basename(scanPath) }] };
const res = await unicodeScan('.', discovery);
for (const f of res.findings || []) {
findings.push({
@ -120,6 +138,8 @@ export async function detectAdversarial(content, opts = {}) {
evidence: f.evidence || f.title || 'unicode anomaly',
});
}
} finally {
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
}
return findings;