feat(goose): add local v1.49 canary routing, smoke gates, and migration docs
Wire Portal and TKMind proxy to loopback Goose v1.49 via canary env blocks, with verification scripts, Phase 2/3 evidence baselines, and rollback runbooks so local upgrade stays isolated from stable 1.41 and production. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Smoke: aider + openhands platform extensions load on local Goose v1.49.
|
||||
*/
|
||||
import { Agent, fetch } from 'undici';
|
||||
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
||||
const pollMs = Number(process.env.GOOSE_V149_EXECUTOR_POLL_MS || 500);
|
||||
const timeoutMs = Number(process.env.GOOSE_V149_EXECUTOR_TIMEOUT_MS || 30_000);
|
||||
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_EXECUTOR_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const base = `https://${host}:${port}`;
|
||||
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
async function apiFetch(pathname, init = {}) {
|
||||
const headers = {
|
||||
...(init.headers ?? {}),
|
||||
'X-Secret-Key': secret,
|
||||
};
|
||||
if (init.body && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return fetch(`${base}${pathname}`, {
|
||||
...init,
|
||||
headers,
|
||||
dispatcher,
|
||||
});
|
||||
}
|
||||
|
||||
async function apiJson(pathname, body) {
|
||||
const response = await apiFetch(pathname, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`);
|
||||
}
|
||||
return text.trim() ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async function waitForTool(sessionId, extensionName, toolName) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const response = await apiFetch(
|
||||
`/agent/tools?session_id=${encodeURIComponent(sessionId)}&extension_name=${encodeURIComponent(extensionName)}`,
|
||||
);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`/agent/tools ${response.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
const tools = JSON.parse(text);
|
||||
if (tools.some((tool) => tool.name === toolName)) {
|
||||
return tools.map((tool) => tool.name);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
throw new Error(`${toolName} not ready within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await apiJson('/agent/start', {
|
||||
working_dir: workingDir,
|
||||
extension_overrides: [
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'aider',
|
||||
description: 'Aider coding delegate',
|
||||
available_tools: ['code'],
|
||||
},
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'openhands',
|
||||
description: 'OpenHands coding delegate',
|
||||
available_tools: ['code'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!session?.id) {
|
||||
throw new Error('missing session id from /agent/start');
|
||||
}
|
||||
|
||||
const aiderTools = await waitForTool(session.id, 'aider', 'aider__code');
|
||||
const openHandsTools = await waitForTool(session.id, 'openhands', 'openhands__code');
|
||||
|
||||
console.log(
|
||||
`GOOSE_V149_EXECUTOR_OK: session=${session.id} aider=${aiderTools.length} openhands=${openHandsTools.length}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_EXECUTOR_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user