fix(extension): HTML 패치 안전성 강화 — pre-patch backup + 구조 검증 + 자동 복원

This commit is contained in:
Variet Worker
2026-03-12 17:55:42 +09:00
parent bb6c03e957
commit 6d8c6f182c
2 changed files with 77 additions and 18 deletions

View File

@@ -414,6 +414,15 @@
### [2026-03-12] workbench.html 0-byte 파괴 — AG 새 창 먹통 ### [2026-03-12] workbench.html 0-byte 파괴 — AG 새 창 먹통
- **증상**: AG 새 창 열면 화면 먹통 (빈 화면). 봇을 꺼도 복구 안 됨 - **증상**: AG 새 창 열면 화면 먹통 (빈 화면). 봇을 꺼도 복구 안 됨
- **원인**: 멀티 윈도우 환경(gravity_control + variet_agent + edf)에서 3개 Extension 인스턴스가 동시에 `workbench.html`을 읽고/패치/쓰기. 한 인스턴스가 `writeFileSync` 중에 다른 인스턴스가 `readFileSync` → 빈 문자열 반환 → 파이프라인 처리 후 0 bytes로 덮어쓰기 - **원인**: 멀티 윈도우 환경(gravity_control + variet_agent + edf)에서 3개 Extension 인스턴스가 동시에 `workbench.html`을 읽고/패치/쓰기. 한 인스턴스가 `writeFileSync` 중에 다른 인스턴스가 `readFileSync` → 빈 문자열 반환 → 파이프라인 처리 후 0 bytes로 덮어쓰기
- **해결**: (1) `workbench-jetski-agent.html`에서 복원 (`jetskiAgent.js``workbench.js` 치환), (2) `product.json` 체크섬 갱신, (3) `CachedData` 삭제, (4) Extension에 pre-read/pre-write 안전 가드 추가 (500 bytes 미만 또는 `<!DOCTYPE html>` 없으면 패치 스킵) - **해결**: (1) pre-patch backup (.orig 파일) 생성, (2) 파일별 구조 검증 (requiredMarker), (3) 손상/잘못된 타입 감지 시 .orig에서 자동 복원
- **주의**: **AG 풀 재시작 필수**. 멀티 윈도우 환경에서 HTML 패치 race condition은 근본적으로 파일 잠금 없이는 완전 해결 불가 — 안전 가드로 피해 최소화 - **주의**: **AG 풀 재시작 필수**. 멀티 윈도우 환경에서 HTML 패치 race condition은 근본적으로 파일 잠금 없이는 완전 해결 불가 — 안전 가드로 피해 최소화
### [2026-03-12] workbench.html 크로스 복원 — CSS 미로딩으로 레이아웃 깨짐
- **증상**: AG 재시작 후 아이콘/요소는 보이지만 전부 왼쪽으로 쏠려서 정렬 안 됨. 레이아웃 완전 깨짐
- **원인**: `workbench.html``workbench-jetski-agent.html`에서 복원할 때 JS만 교체 (`jetskiAgent.js``workbench.js`)하고 **CSS 참조를 교체하지 않음**. 두 파일은 완전히 다른 CSS를 사용:
- `workbench.html``workbench.desktop.main.css` (VS Code 기본 레이아웃)
- `workbench-jetski-agent.html``tw-base.tailwind.css` + `jetskiMain.tailwind.css` (Jetski Agent 전용)
잘못된 CSS가 로드되면 JS는 정상 작동하나 레이아웃이 완전히 무너짐
- **해결**: (1) Extension에 파일별 `requiredMarker` 검증 추가 (workbench.html은 `workbench.desktop.main.css`, jetski는 `jetskiMain.tailwind.css` 필수), (2) 첫 패치 전 `.orig` 백업 자동 생성, (3) 손상 또는 잘못된 타입 감지 시 `.orig`에서 자동 복원. **두 HTML 파일은 절대 크로스 복원 불가**
- **주의**: `workbench.html``workbench-jetski-agent.html`**교환 불가능**. CSS 경로, JS 엔트리 포인트, CSP 세부 설정이 모두 다름. 수동 복원 시도 금지 — Extension의 자동 복원 로직에 의존할 것

View File

@@ -431,21 +431,71 @@ async function setupApprovalObserver() {
// workbench.html — loaded by DevTools/standard mode // workbench.html — loaded by DevTools/standard mode
// workbench-jetski-agent.html — loaded by AG agent mode // workbench-jetski-agent.html — loaded by AG agent mode
const scriptDir = path.dirname(scriptPath); const scriptDir = path.dirname(scriptPath);
const htmlFiles = ['workbench.html', 'workbench-jetski-agent.html']; // Each HTML file has DIFFERENT CSS/JS entry points — they are NOT interchangeable:
for (const htmlFileName of htmlFiles) { // workbench.html → workbench.desktop.main.css + workbench.js
const htmlPath = path.join(scriptDir, htmlFileName); // workbench-jetski-agent.html → tw-base.tailwind.css + jetskiMain.tailwind.css + jetskiAgent.js
// Cross-restoring between them causes CSS to not load → layout broken (elements visible but all shifted left).
const htmlFileSpecs = [
{
name: 'workbench.html',
requiredMarker: 'workbench.desktop.main.css', // CSS unique to this file
requiredScript: 'workbench.js', // JS entry point
},
{
name: 'workbench-jetski-agent.html',
requiredMarker: 'jetskiMain.tailwind.css', // CSS unique to this file
requiredScript: 'jetskiAgent.js', // JS entry point
},
];
for (const spec of htmlFileSpecs) {
const htmlPath = path.join(scriptDir, spec.name);
const backupPath = htmlPath + '.orig';
try { try {
if (!fs.existsSync(htmlPath)) { if (!fs.existsSync(htmlPath)) {
logToFile(`[OBSERVER] ${htmlFileName} not found — skipping`); logToFile(`[OBSERVER] ${spec.name} not found — skipping`);
continue; continue;
} }
let html = fs.readFileSync(htmlPath, 'utf8'); let html = fs.readFileSync(htmlPath, 'utf8');
// SAFETY: Refuse to patch if file is empty or suspiciously small // ── BACKUP: Save original before first-ever patch ──
// (race condition: another extension instance may be mid-write) // Only backup if the file looks valid AND hasn't been backed up yet.
if (html.length < 500 || !html.includes('<!DOCTYPE html>')) { if (!fs.existsSync(backupPath)
logToFile(`[OBSERVER] ${htmlFileName} appears corrupt or empty (${html.length} bytes) — SKIPPING to prevent further damage`); && html.length >= 500
continue; && html.includes('<!DOCTYPE html>')
&& html.includes(spec.requiredMarker)) {
fs.writeFileSync(backupPath, html, 'utf8');
logToFile(`[OBSERVER] ${spec.name} backed up to .orig (${html.length} bytes)`);
}
// ── SAFETY: Refuse to patch if file is corrupt, empty, or wrong type ──
// Race condition: another extension instance may be mid-write (0-byte).
// Wrong type: restored from the other HTML file (different CSS/JS refs).
const isCorrupt = html.length < 500 || !html.includes('<!DOCTYPE html>');
const isWrongType = !isCorrupt && !html.includes(spec.requiredMarker);
if (isCorrupt || isWrongType) {
const reason = isCorrupt
? `corrupt/empty (${html.length} bytes)`
: `wrong type (missing ${spec.requiredMarker})`;
logToFile(`[OBSERVER] ${spec.name} detected ${reason}`);
// Try to restore from backup
if (fs.existsSync(backupPath)) {
const backup = fs.readFileSync(backupPath, 'utf8');
if (backup.length >= 500
&& backup.includes('<!DOCTYPE html>')
&& backup.includes(spec.requiredMarker)) {
fs.writeFileSync(htmlPath, backup, 'utf8');
html = backup;
logToFile(`[OBSERVER] ${spec.name} RESTORED from .orig backup (${backup.length} bytes) ✅`);
} else {
logToFile(`[OBSERVER] ${spec.name} .orig backup also invalid — SKIPPING`);
continue;
}
} else {
logToFile(`[OBSERVER] ${spec.name} no .orig backup available — SKIPPING to prevent further damage`);
continue;
}
} }
// CRITICAL: Patch CSP to allow inline scripts. // CRITICAL: Patch CSP to allow inline scripts.
@@ -456,7 +506,7 @@ async function setupApprovalObserver() {
/(script-src\s[^;]*?)('self')/, /(script-src\s[^;]*?)('self')/,
"$1$2\n\t\t\t\t\t'unsafe-inline'" "$1$2\n\t\t\t\t\t'unsafe-inline'"
); );
logToFile(`[OBSERVER] ${htmlFileName} CSP patched: added 'unsafe-inline' to script-src`); logToFile(`[OBSERVER] ${spec.name} CSP patched: added 'unsafe-inline' to script-src`);
} }
// Remove old external script tag if present (legacy, cannot be served) // Remove old external script tag if present (legacy, cannot be served)
@@ -469,7 +519,7 @@ async function setupApprovalObserver() {
extMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?' extMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?'
); );
html = html.replace(extRe, ''); html = html.replace(extRe, '');
logToFile(`[OBSERVER] removed external script tag from ${htmlFileName}`); logToFile(`[OBSERVER] removed external script tag from ${spec.name}`);
} }
// Insert or update inline script // Insert or update inline script
@@ -484,20 +534,20 @@ async function setupApprovalObserver() {
); );
html = html.replace(re, html = html.replace(re,
`${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`); `${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`);
logToFile(`[OBSERVER] ${htmlFileName} inline script UPDATED`); logToFile(`[OBSERVER] ${spec.name} inline script UPDATED`);
} else { } else {
html = html.replace('</html>', html = html.replace('</html>',
`\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`); `\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`);
logToFile(`[OBSERVER] ${htmlFileName} inline script INSERTED`); logToFile(`[OBSERVER] ${spec.name} inline script INSERTED`);
} }
// SAFETY: Final validation before write — never write empty or invalid HTML // SAFETY: Final validation before write
if (html.length < 500 || !html.includes('<!DOCTYPE html>')) { if (html.length < 500 || !html.includes('<!DOCTYPE html>') || !html.includes(spec.requiredMarker)) {
logToFile(`[OBSERVER] ${htmlFileName} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`); logToFile(`[OBSERVER] ${spec.name} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`);
continue; continue;
} }
fs.writeFileSync(htmlPath, html, 'utf8'); fs.writeFileSync(htmlPath, html, 'utf8');
} catch (e: any) { } catch (e: any) {
logToFile(`[OBSERVER] ${htmlFileName} patch error: ${e.message}`); logToFile(`[OBSERVER] ${spec.name} patch error: ${e.message}`);
} }
} }
} }