From 6d8c6f182c1e3c7465a6664def873bcb32ea3c91 Mon Sep 17 00:00:00 2001 From: Variet Worker Date: Thu, 12 Mar 2026 17:55:42 +0900 Subject: [PATCH] =?UTF-8?q?fix(extension):=20HTML=20=ED=8C=A8=EC=B9=98=20?= =?UTF-8?q?=EC=95=88=EC=A0=84=EC=84=B1=20=EA=B0=95=ED=99=94=20=E2=80=94=20?= =?UTF-8?q?pre-patch=20backup=20+=20=EA=B5=AC=EC=A1=B0=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20+=20=EC=9E=90=EB=8F=99=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/references/known-issues.md | 11 +++- extension/src/extension.ts | 84 ++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index 2e5fe02..fc1c7f3 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -414,6 +414,15 @@ ### [2026-03-12] workbench.html 0-byte 파괴 — AG 새 창 먹통 - **증상**: AG 새 창 열면 화면 먹통 (빈 화면). 봇을 꺼도 복구 안 됨 - **원인**: 멀티 윈도우 환경(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 미만 또는 `` 없으면 패치 스킵) +- **해결**: (1) pre-patch backup (.orig 파일) 생성, (2) 파일별 구조 검증 (requiredMarker), (3) 손상/잘못된 타입 감지 시 .orig에서 자동 복원 - **주의**: **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의 자동 복원 로직에 의존할 것 + diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 2790e71..d6714dd 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -431,21 +431,71 @@ async function setupApprovalObserver() { // workbench.html — loaded by DevTools/standard mode // workbench-jetski-agent.html — loaded by AG agent mode const scriptDir = path.dirname(scriptPath); - const htmlFiles = ['workbench.html', 'workbench-jetski-agent.html']; - for (const htmlFileName of htmlFiles) { - const htmlPath = path.join(scriptDir, htmlFileName); + // Each HTML file has DIFFERENT CSS/JS entry points — they are NOT interchangeable: + // workbench.html → workbench.desktop.main.css + workbench.js + // 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 { if (!fs.existsSync(htmlPath)) { - logToFile(`[OBSERVER] ${htmlFileName} not found — skipping`); + logToFile(`[OBSERVER] ${spec.name} not found — skipping`); continue; } let html = fs.readFileSync(htmlPath, 'utf8'); - // SAFETY: Refuse to patch if file is empty or suspiciously small - // (race condition: another extension instance may be mid-write) - if (html.length < 500 || !html.includes('')) { - logToFile(`[OBSERVER] ${htmlFileName} appears corrupt or empty (${html.length} bytes) — SKIPPING to prevent further damage`); - continue; + // ── BACKUP: Save original before first-ever patch ── + // Only backup if the file looks valid AND hasn't been backed up yet. + if (!fs.existsSync(backupPath) + && html.length >= 500 + && html.includes('') + && 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(''); + 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('') + && 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. @@ -456,7 +506,7 @@ async function setupApprovalObserver() { /(script-src\s[^;]*?)('self')/, "$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) @@ -469,7 +519,7 @@ async function setupApprovalObserver() { extMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?' ); 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 @@ -484,20 +534,20 @@ async function setupApprovalObserver() { ); html = html.replace(re, `${inlineMarkerStart}\n\n${inlineMarkerEnd}`); - logToFile(`[OBSERVER] ${htmlFileName} inline script UPDATED`); + logToFile(`[OBSERVER] ${spec.name} inline script UPDATED`); } else { html = html.replace('', `\n${inlineMarkerStart}\n\n${inlineMarkerEnd}\n`); - logToFile(`[OBSERVER] ${htmlFileName} inline script INSERTED`); + logToFile(`[OBSERVER] ${spec.name} inline script INSERTED`); } - // SAFETY: Final validation before write — never write empty or invalid HTML - if (html.length < 500 || !html.includes('')) { - logToFile(`[OBSERVER] ${htmlFileName} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`); + // SAFETY: Final validation before write + if (html.length < 500 || !html.includes('') || !html.includes(spec.requiredMarker)) { + logToFile(`[OBSERVER] ${spec.name} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`); continue; } fs.writeFileSync(htmlPath, html, 'utf8'); } catch (e: any) { - logToFile(`[OBSERVER] ${htmlFileName} patch error: ${e.message}`); + logToFile(`[OBSERVER] ${spec.name} patch error: ${e.message}`); } } }