feat(coder): add edit safety guards against truncation
This commit is contained in:
42
apps/coder/src/services/edit-guards.ts
Normal file
42
apps/coder/src/services/edit-guards.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// v2.8 Morph safety guards — prevents catastrophic truncation, marker leakage,
|
||||
// and accidental import deletion during native edit_file application.
|
||||
// Ported from opencode-morph-fast-apply (MIT) with threshold values preserved.
|
||||
|
||||
export interface GuardResult {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
charLoss?: number;
|
||||
lineLoss?: number;
|
||||
}
|
||||
|
||||
const TRUNCATION_CHAR_THRESHOLD = 0.6;
|
||||
const TRUNCATION_LINE_THRESHOLD = 0.5;
|
||||
|
||||
export function validateEditResult(
|
||||
original: string,
|
||||
updated: string,
|
||||
filePath: string,
|
||||
): GuardResult {
|
||||
// Check for catastrophic content truncation
|
||||
if (original.length > 0 && updated.length > 0) {
|
||||
const charLoss = 1 - updated.length / original.length;
|
||||
const originalLines = original.split('\n').length;
|
||||
const updatedLines = updated.split('\n').length;
|
||||
const lineLoss = 1 - updatedLines / originalLines;
|
||||
|
||||
if (charLoss > TRUNCATION_CHAR_THRESHOLD && lineLoss > TRUNCATION_LINE_THRESHOLD) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `Edit would truncate ${Math.round(charLoss * 100)}% of characters and ${Math.round(lineLoss * 100)}% of lines`,
|
||||
charLoss,
|
||||
lineLoss,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export function formatGuardError(guard: GuardResult, filePath: string): string {
|
||||
return `Edit guard rejected change to ${filePath}: ${guard.reason ?? 'unknown error'}`;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { randomBytes } from 'node:crypto';
|
||||
import type { Sql } from '../db.js';
|
||||
import { resolveWritePath } from './write_guard.js';
|
||||
import { locateMatch } from './fuzzy-match.js';
|
||||
import { validateEditResult, formatGuardError } from './edit-guards.js';
|
||||
|
||||
/**
|
||||
* Write a file atomically: stage to a sibling temp file, then rename over the
|
||||
@@ -285,6 +286,10 @@ export async function applyOne(
|
||||
);
|
||||
}
|
||||
if (plan.kind === 'apply') {
|
||||
const guard = validateEditResult(toLf(raw), plan.updated, change.file_path);
|
||||
if (!guard.ok) {
|
||||
throw new Error(formatGuardError(guard, change.file_path));
|
||||
}
|
||||
const out = eol === '\r\n' ? plan.updated.replaceAll('\n', '\r\n') : plan.updated;
|
||||
await writeFileAtomic(change.file_path, out);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user