fix(bridge): v0.3.5 — inline script + deterministic port + auto-checksum
- vscode-file:// refuses custom .js files → inline script into HTML - Random port → deterministic port from project name hash (gravity_control=34332) - Hardcoded port in renderer script for immediate discovery - Auto-update product.json SHA256 checksums after HTML modification - Bump version 0.2.0 → 0.3.5
This commit is contained in:
@@ -51,6 +51,7 @@ const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
const os = __importStar(require("os"));
|
||||
const cp = __importStar(require("child_process"));
|
||||
const crypto = __importStar(require("crypto"));
|
||||
// ─── File-based logging (AI can read directly) ───
|
||||
function logToFile(msg) {
|
||||
const ts = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
@@ -73,6 +74,7 @@ let statusBar;
|
||||
let bridgePath;
|
||||
let projectName;
|
||||
let isActive = false;
|
||||
let deterministicPort = 0; // derived from projectName, consistent across restarts
|
||||
let watcher = null;
|
||||
let commandsWatcher = null;
|
||||
const sentPendingIds = new Set();
|
||||
@@ -287,26 +289,44 @@ async function setupApprovalObserver() {
|
||||
logToFile('[OBSERVER] workbench.html patched (needs reload)');
|
||||
}
|
||||
// Also patch workbench-jetski-agent.html (Antigravity's actual entry point!)
|
||||
// IMPORTANT: vscode-file:// protocol does NOT serve custom .js files,
|
||||
// so we INLINE the script content directly into the HTML.
|
||||
const scriptDir = path.dirname(scriptPath);
|
||||
const jetskiHtml = path.join(scriptDir, 'workbench-jetski-agent.html');
|
||||
const scriptBasename = path.basename(scriptPath);
|
||||
try {
|
||||
if (fs.existsSync(jetskiHtml)) {
|
||||
let html = fs.readFileSync(jetskiHtml, 'utf8');
|
||||
if (!html.includes(scriptBasename)) {
|
||||
html = html.replace('</html>', `\n<!-- AG SDK [variet-gravity-bridge] -->\n<script src="./${scriptBasename}"></script>\n<!-- /AG SDK [variet-gravity-bridge] -->\n</html>`);
|
||||
fs.writeFileSync(jetskiHtml, html, 'utf8');
|
||||
logToFile('[OBSERVER] workbench-jetski-agent.html PATCHED');
|
||||
// Remove old external script tag if present (from previous versions)
|
||||
const oldScriptBasename = path.basename(scriptPath);
|
||||
if (html.includes(`src="./${oldScriptBasename}"`)) {
|
||||
html = html.replace(/\n?<!-- AG SDK \[variet-gravity-bridge\] -->\n?<script src="[^"]*"><\/script>\n?<!-- \/AG SDK \[variet-gravity-bridge\] -->\n?/, '');
|
||||
logToFile('[OBSERVER] removed old external <script src> tag from jetski HTML');
|
||||
}
|
||||
// Insert or update inline script
|
||||
const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->';
|
||||
const inlineMarkerEnd = '<!-- /AG SDK INLINE [variet-gravity-bridge] -->';
|
||||
if (html.includes(inlineMarkerStart)) {
|
||||
// Replace existing inline script with updated content
|
||||
const re = new RegExp(inlineMarkerStart.replace(/[[\]]/g, '\\$&') +
|
||||
'[\\s\\S]*?' +
|
||||
inlineMarkerEnd.replace(/[[\]]/g, '\\$&'));
|
||||
html = html.replace(re, `${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`);
|
||||
logToFile('[OBSERVER] jetski HTML inline script UPDATED');
|
||||
}
|
||||
else {
|
||||
logToFile('[OBSERVER] workbench-jetski-agent.html already has script tag');
|
||||
// First time: insert before </html>
|
||||
html = html.replace('</html>', `\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`);
|
||||
logToFile('[OBSERVER] jetski HTML inline script INSERTED');
|
||||
}
|
||||
fs.writeFileSync(jetskiHtml, html, 'utf8');
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[OBSERVER] jetski patch error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
// 4. Update product.json checksums so vscode-file:// serves our patched files
|
||||
updateProductChecksums();
|
||||
try {
|
||||
integration.enableAutoRepair();
|
||||
}
|
||||
@@ -322,9 +342,78 @@ async function setupApprovalObserver() {
|
||||
logToFile(`[OBSERVER] setup error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
// ─── Product.json Checksum Auto-Update ───
|
||||
// vscode-file:// protocol validates SHA256 checksums in product.json.
|
||||
// If a file's checksum doesn't match, Electron serves the ORIGINAL cached version.
|
||||
// This function recalculates checksums for files we modify (HTML files with <script> tags).
|
||||
function updateProductChecksums() {
|
||||
try {
|
||||
// Find product.json (2 levels up from workbench dir: resources/app/product.json)
|
||||
const patcher = sdk?.integration?._patcher;
|
||||
if (!patcher || typeof patcher.getWorkbenchDir !== 'function') {
|
||||
logToFile('[CHECKSUM] no patcher/workbenchDir — skipping');
|
||||
return;
|
||||
}
|
||||
const workbenchDir = patcher.getWorkbenchDir();
|
||||
// workbenchDir = .../resources/app/out/vs/code/electron-browser/workbench
|
||||
// product.json = .../resources/app/product.json (5 levels up from workbench)
|
||||
const appDir = path.resolve(workbenchDir, '..', '..', '..', '..', '..');
|
||||
const productJsonPath = path.join(appDir, 'product.json');
|
||||
if (!fs.existsSync(productJsonPath)) {
|
||||
logToFile(`[CHECKSUM] product.json not found at ${productJsonPath}`);
|
||||
return;
|
||||
}
|
||||
// Read product.json (may have BOM)
|
||||
let raw = fs.readFileSync(productJsonPath, 'utf8');
|
||||
if (raw.charCodeAt(0) === 0xFEFF)
|
||||
raw = raw.substring(1);
|
||||
const product = JSON.parse(raw);
|
||||
if (!product.checksums) {
|
||||
logToFile('[CHECKSUM] no checksums section in product.json');
|
||||
return;
|
||||
}
|
||||
// Files we may modify (relative key in product.json → absolute path)
|
||||
const filesToCheck = {
|
||||
'vs/code/electron-browser/workbench/workbench.html': path.join(workbenchDir, 'workbench.html'),
|
||||
'vs/code/electron-browser/workbench/workbench-jetski-agent.html': path.join(workbenchDir, 'workbench-jetski-agent.html'),
|
||||
};
|
||||
let updated = false;
|
||||
for (const [key, filePath] of Object.entries(filesToCheck)) {
|
||||
if (!product.checksums[key])
|
||||
continue; // not in checksums, skip
|
||||
if (!fs.existsSync(filePath))
|
||||
continue;
|
||||
const fileBytes = fs.readFileSync(filePath);
|
||||
const hash = crypto.createHash('sha256').update(fileBytes).digest('base64').replace(/=+$/, '');
|
||||
if (product.checksums[key] !== hash) {
|
||||
logToFile(`[CHECKSUM] updating ${key}: ${product.checksums[key].substring(0, 12)}... → ${hash.substring(0, 12)}...`);
|
||||
product.checksums[key] = hash;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
fs.writeFileSync(productJsonPath, JSON.stringify(product, null, '\t'), 'utf8');
|
||||
logToFile('[CHECKSUM] product.json updated ✅');
|
||||
}
|
||||
else {
|
||||
logToFile('[CHECKSUM] all checksums already match ✅');
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[CHECKSUM] error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
// ─── HTTP Bridge Server (Extension Host → Renderer communication) ───
|
||||
let observerHttpServer = null;
|
||||
const pendingResponses = new Map();
|
||||
/** Derive a deterministic port from project name (range 10000-60000) */
|
||||
function getDeterministicPort(name) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
|
||||
}
|
||||
return 10000 + (Math.abs(hash) % 50000);
|
||||
}
|
||||
function startObserverHttpBridge() {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
@@ -406,33 +495,53 @@ function startObserverHttpBridge() {
|
||||
res.writeHead(404);
|
||||
res.end('not found');
|
||||
});
|
||||
// Listen on random port
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const port = server.address().port;
|
||||
observerHttpServer = server;
|
||||
logToFile(`[HTTP] bridge server started on port ${port}`);
|
||||
// Write port to shared ports JSON (multi-bridge support)
|
||||
const patcher = sdk.integration?._patcher;
|
||||
if (patcher && typeof patcher.getWorkbenchDir === 'function') {
|
||||
const workbenchDir = patcher.getWorkbenchDir();
|
||||
const portsFile = path.join(workbenchDir, 'ag-bridge-ports.json');
|
||||
let portsData = {};
|
||||
try {
|
||||
if (fs.existsSync(portsFile)) {
|
||||
portsData = JSON.parse(fs.readFileSync(portsFile, 'utf-8'));
|
||||
// Listen on deterministic port (derived from projectName), fallback to random
|
||||
deterministicPort = getDeterministicPort(projectName);
|
||||
const tryListen = (targetPort) => {
|
||||
server.listen(targetPort, '127.0.0.1', () => {
|
||||
const port = server.address().port;
|
||||
observerHttpServer = server;
|
||||
logToFile(`[HTTP] bridge server started on port ${port}`);
|
||||
// Write port to shared ports JSON (multi-bridge support)
|
||||
const patcher = sdk.integration?._patcher;
|
||||
if (patcher && typeof patcher.getWorkbenchDir === 'function') {
|
||||
const workbenchDir = patcher.getWorkbenchDir();
|
||||
const portsFile = path.join(workbenchDir, 'ag-bridge-ports.json');
|
||||
let portsData = {};
|
||||
try {
|
||||
if (fs.existsSync(portsFile)) {
|
||||
portsData = JSON.parse(fs.readFileSync(portsFile, 'utf-8'));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
portsData[projectName] = port;
|
||||
fs.writeFileSync(portsFile, JSON.stringify(portsData), 'utf-8');
|
||||
logToFile(`[HTTP] ports JSON updated → ${portsFile} (${projectName}=${port})`);
|
||||
}
|
||||
catch { }
|
||||
portsData[projectName] = port;
|
||||
fs.writeFileSync(portsFile, JSON.stringify(portsData), 'utf-8');
|
||||
logToFile(`[HTTP] ports JSON updated → ${portsFile} (${projectName}=${port})`);
|
||||
}
|
||||
resolve(port);
|
||||
});
|
||||
resolve(port);
|
||||
});
|
||||
};
|
||||
server.on('error', (e) => {
|
||||
if (e.code === 'EADDRINUSE' && deterministicPort > 0) {
|
||||
logToFile(`[HTTP] deterministic port ${deterministicPort} in use, trying random...`);
|
||||
deterministicPort = 0;
|
||||
const server2 = require('http').createServer(server._events.request);
|
||||
observerHttpServer = server2;
|
||||
server2.on('error', (e2) => {
|
||||
logToFile(`[HTTP] random port also failed: ${e2.message}`);
|
||||
resolve(0);
|
||||
});
|
||||
server2.listen(0, '127.0.0.1', () => {
|
||||
const port = server2.address().port;
|
||||
logToFile(`[HTTP] bridge server started on RANDOM port ${port}`);
|
||||
resolve(port);
|
||||
});
|
||||
return;
|
||||
}
|
||||
logToFile(`[HTTP] server error: ${e.message}`);
|
||||
resolve(0);
|
||||
});
|
||||
tryListen(deterministicPort);
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[HTTP] server failed: ${e.message}`);
|
||||
@@ -455,40 +564,67 @@ function generateApprovalObserverScript(_port) {
|
||||
function log(m){console.log('[GB Observer] '+m);}
|
||||
log('v2 Script loaded — discovering bridge port...');
|
||||
|
||||
// ── Multi-Port Discovery: reads ag-bridge-ports.json, tries ALL bridges ──
|
||||
// ── Port Discovery: try hardcoded port FIRST, then JSON fallback ──
|
||||
// vscode-file:// protocol blocks .json files, so XHR to ag-bridge-ports.json
|
||||
// returns 404. The extension embeds the known port at script generation time.
|
||||
var HARDCODED_PORT=${_port};
|
||||
|
||||
function tryPing(port,cb){
|
||||
try{
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
||||
xhr.timeout=2000;
|
||||
xhr.send();
|
||||
if(xhr.status===200&&xhr.responseText==='pong'){cb(true);return;}
|
||||
}catch(e){}
|
||||
cb(false);
|
||||
}
|
||||
|
||||
function discoverPort(cb){
|
||||
var attempts=0;
|
||||
var timer=setInterval(function(){
|
||||
attempts++;
|
||||
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
||||
try{
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET','./ag-bridge-ports.json?t='+Date.now(),false);
|
||||
xhr.send();
|
||||
if(xhr.status===200){
|
||||
var ports=JSON.parse(xhr.responseText);
|
||||
var keys=Object.keys(ports);
|
||||
for(var i=0;i<keys.length;i++){
|
||||
var port=ports[keys[i]];
|
||||
if(port>0&&port<65536){
|
||||
// Try ping on each port
|
||||
try{
|
||||
var xhr2=new XMLHttpRequest();
|
||||
xhr2.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
||||
xhr2.timeout=1000;
|
||||
xhr2.send();
|
||||
if(xhr2.status===200&&xhr2.responseText==='pong'){
|
||||
clearInterval(timer);
|
||||
log('Port discovered: '+port+' (project='+keys[i]+')');
|
||||
cb(port);
|
||||
return;
|
||||
}
|
||||
}catch(e2){}
|
||||
// Strategy 1: Try hardcoded port immediately
|
||||
log('Trying hardcoded port '+HARDCODED_PORT+'...');
|
||||
tryPing(HARDCODED_PORT,function(ok){
|
||||
if(ok){log('Port discovered (hardcoded): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
|
||||
log('Hardcoded port failed, falling back to JSON + port scan...');
|
||||
|
||||
// Strategy 2: JSON file fallback + port scan
|
||||
var attempts=0;
|
||||
var timer=setInterval(function(){
|
||||
attempts++;
|
||||
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
||||
// Try hardcoded port again (server may start late)
|
||||
tryPing(HARDCODED_PORT,function(ok2){
|
||||
if(ok2){clearInterval(timer);log('Port discovered (hardcoded retry): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
|
||||
});
|
||||
// Try JSON file (may work in future Electron versions)
|
||||
try{
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET','./ag-bridge-ports.json?t='+Date.now(),false);
|
||||
xhr.send();
|
||||
if(xhr.status===200){
|
||||
var ports=JSON.parse(xhr.responseText);
|
||||
var keys=Object.keys(ports);
|
||||
for(var i=0;i<keys.length;i++){
|
||||
var port=ports[keys[i]];
|
||||
if(port>0&&port<65536){
|
||||
try{
|
||||
var xhr2=new XMLHttpRequest();
|
||||
xhr2.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
||||
xhr2.timeout=1000;
|
||||
xhr2.send();
|
||||
if(xhr2.status===200&&xhr2.responseText==='pong'){
|
||||
clearInterval(timer);
|
||||
log('Port discovered (JSON): '+port+' (project='+keys[i]+')');
|
||||
cb(port);
|
||||
return;
|
||||
}
|
||||
}catch(e2){}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
},2000);
|
||||
}catch(e){}
|
||||
},2000);
|
||||
});
|
||||
}
|
||||
|
||||
discoverPort(function(port){
|
||||
|
||||
Reference in New Issue
Block a user