fix(extension): HTML 패치 안전성 강화 — pre-patch backup + 구조 검증 + 자동 복원
This commit is contained in:
@@ -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의 자동 복원 로직에 의존할 것
|
||||||
|
|
||||||
|
|||||||
@@ -431,22 +431,72 @@ 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
|
||||||
|
&& 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;
|
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.
|
||||||
// Default CSP has script-src 'self' 'unsafe-eval' blob: — NO 'unsafe-inline'.
|
// Default CSP has script-src 'self' 'unsafe-eval' blob: — NO 'unsafe-inline'.
|
||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user