716ef407fe
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>
168 lines
5.5 KiB
JavaScript
168 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Capture Goose v1.49 local baseline samples (status, sessions, optional SSE reply).
|
|
*/
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { Readable } from 'node:stream';
|
|
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 base = `https://${host}:${port}`;
|
|
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
|
|
|
function usage() {
|
|
return [
|
|
'Usage: node scripts/capture-goose-v149-baseline.mjs [--output <dir>] [--skip-reply]',
|
|
'',
|
|
'Captures /status, /sessions, and optional reply SSE terminal event for Phase 0 baseline.',
|
|
].join('\n');
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = { output: '', skipReply: false };
|
|
for (let i = 2; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (arg === '--output' && argv[i + 1]) args.output = argv[++i];
|
|
else if (arg === '--skip-reply') args.skipReply = true;
|
|
else if (arg === '--help' || arg === '-h') {
|
|
console.log(usage());
|
|
process.exit(0);
|
|
} else {
|
|
console.error(`Unknown argument: ${arg}`);
|
|
console.error(usage());
|
|
process.exit(2);
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
if (blockedHosts.includes(host)) {
|
|
console.error(`GOOSE_V149_BASELINE_FAIL: refusing production host ${host}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
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 curlJson(url) {
|
|
const body = execFileSync('curl', ['-sk', '--connect-timeout', '5', url], { encoding: 'utf8' }).trim();
|
|
return body === 'ok' ? { status: 'ok' } : JSON.parse(body);
|
|
}
|
|
|
|
async function captureReplySample(sessionId) {
|
|
const requestId = randomUUID();
|
|
const events = [];
|
|
const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, {
|
|
method: 'GET',
|
|
headers: { Accept: 'text/event-stream' },
|
|
});
|
|
if (!eventsResponse.ok || !eventsResponse.body) {
|
|
throw new Error(`events ${eventsResponse.status}`);
|
|
}
|
|
|
|
const replyResponse = await apiFetch(`/sessions/${sessionId}/reply`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
request_id: requestId,
|
|
user_message: {
|
|
role: 'user',
|
|
created: Date.now(),
|
|
content: [{ type: 'text', text: 'baseline ping' }],
|
|
metadata: { userVisible: true, agentVisible: true, displayText: 'baseline ping' },
|
|
},
|
|
}),
|
|
});
|
|
replyResponse.body?.cancel?.();
|
|
|
|
const reader = Readable.fromWeb(eventsResponse.body);
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
const deadline = Date.now() + Number(process.env.GOOSE_V149_BASELINE_REPLY_TIMEOUT_MS || 30_000);
|
|
|
|
for await (const chunk of reader) {
|
|
if (Date.now() > deadline) break;
|
|
buffer += decoder.decode(chunk, { stream: true });
|
|
const frames = buffer.split('\n\n');
|
|
buffer = frames.pop() ?? '';
|
|
for (const frame of frames) {
|
|
let data = '';
|
|
for (const line of frame.split('\n')) {
|
|
if (line.startsWith('data:')) data += line.slice(5).trim();
|
|
}
|
|
if (!data) continue;
|
|
let event;
|
|
try {
|
|
event = JSON.parse(data);
|
|
} catch {
|
|
continue;
|
|
}
|
|
events.push(event);
|
|
const routingId = event.chat_request_id ?? event.request_id;
|
|
if (routingId && routingId !== requestId) continue;
|
|
if (event.type === 'Finish' || event.type === 'Error') {
|
|
return { requestId, outcome: event.type.toLowerCase(), events };
|
|
}
|
|
}
|
|
}
|
|
return { requestId, outcome: 'timeout', events };
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv);
|
|
const capturedAt = new Date().toISOString();
|
|
const status = await curlJson(`${base}/status`);
|
|
const sessions = await curlJson(`${base}/sessions`);
|
|
|
|
const baseline = {
|
|
schemaVersion: 'goose-v149-baseline-v1',
|
|
capturedAt,
|
|
target: base,
|
|
status,
|
|
sessionsSummary: {
|
|
count: Array.isArray(sessions?.sessions) ? sessions.sessions.length : null,
|
|
sample: Array.isArray(sessions?.sessions) ? sessions.sessions.slice(0, 3) : sessions,
|
|
},
|
|
reply: null,
|
|
};
|
|
|
|
if (!args.skipReply) {
|
|
const startResponse = await apiFetch('/agent/start', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ working_dir: process.cwd() }),
|
|
});
|
|
const startBody = await startResponse.json();
|
|
if (startResponse.ok && startBody?.id) {
|
|
baseline.reply = await captureReplySample(startBody.id);
|
|
baseline.reply.sessionId = startBody.id;
|
|
} else {
|
|
baseline.reply = { error: 'agent/start failed', status: startResponse.status, body: startBody };
|
|
}
|
|
}
|
|
|
|
const outputDir = args.output
|
|
? path.resolve(args.output)
|
|
: path.join(process.cwd(), 'docs', 'baselines');
|
|
await fs.mkdir(outputDir, { recursive: true });
|
|
const outputPath = path.join(
|
|
outputDir,
|
|
`goose-v149-runtime-baseline-${capturedAt.replace(/[:.]/g, '-')}.json`,
|
|
);
|
|
await fs.writeFile(outputPath, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8');
|
|
console.log(`GOOSE_V149_BASELINE_OK: ${outputPath}`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`GOOSE_V149_BASELINE_FAIL: ${error.message}`);
|
|
process.exit(1);
|
|
});
|