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>
208 lines
6.6 KiB
JavaScript
208 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Verify Portal boots with Goose v1.49 canary (.env.local block) and routes to :18049.
|
|
* Spawns an isolated Portal on H5_PORT=8083 to avoid disrupting the main dev server.
|
|
*/
|
|
import { spawn } from 'node:child_process';
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
|
|
import { describeGooseCanaryConfig, resolveGooseApiTargetsFromEnv, applyGooseV149CanaryBlockEnv } from './goose-v149-canary.mjs';
|
|
|
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const portalPort = Number(process.env.GOOSE_V149_CANARY_PORTAL_PORT ?? 8083);
|
|
const baseUrl = `http://127.0.0.1:${portalPort}`;
|
|
const startupTimeoutMs = Number(process.env.GOOSE_V149_CANARY_PORTAL_STARTUP_MS ?? 45_000);
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function waitForPortal(fetchImpl, timeoutMs) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const response = await fetchImpl(`${baseUrl}/auth/status`);
|
|
if (response.ok) return true;
|
|
} catch {
|
|
// retry
|
|
}
|
|
await sleep(500);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function requestJson(fetchImpl, url, { method = 'GET', headers = {}, body } = {}) {
|
|
const response = await fetchImpl(url, {
|
|
method,
|
|
headers: {
|
|
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
...headers,
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
const text = await response.text();
|
|
let json = null;
|
|
try {
|
|
json = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
json = null;
|
|
}
|
|
return { ok: response.ok, status: response.status, json, text, headers: response.headers };
|
|
}
|
|
|
|
function readCanaryBlock() {
|
|
const envFile = path.join(root, '.env.local');
|
|
if (!fs.existsSync(envFile)) return false;
|
|
return fs.readFileSync(envFile, 'utf8').includes('# GOOSE_V149_CANARY_BEGIN');
|
|
}
|
|
|
|
async function main() {
|
|
if (!readCanaryBlock()) {
|
|
throw new Error('canary block missing; run: bash scripts/switch-goose-v149-canary.sh on all');
|
|
}
|
|
|
|
const env = { ...process.env };
|
|
loadMemindEnvFiles(root, env);
|
|
applyGooseV149CanaryBlockEnv(env, root);
|
|
env.H5_PORT = String(portalPort);
|
|
env.MEMIND_WORKSPACE_MAINTENANCE = '0';
|
|
|
|
const canary = resolveGooseApiTargetsFromEnv(env);
|
|
if (!canary || canary.primary !== 'https://127.0.0.1:18049') {
|
|
throw new Error(`canary env not pointing to v149: ${JSON.stringify(describeGooseCanaryConfig(env))}`);
|
|
}
|
|
|
|
const server = spawn(process.execPath, ['server.mjs'], {
|
|
cwd: root,
|
|
env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
let logTail = '';
|
|
server.stdout?.on('data', (chunk) => {
|
|
logTail = `${logTail}${chunk}`.slice(-4000);
|
|
});
|
|
server.stderr?.on('data', (chunk) => {
|
|
logTail = `${logTail}${chunk}`.slice(-4000);
|
|
});
|
|
|
|
const cleanup = () => {
|
|
if (!server.killed) server.kill('SIGTERM');
|
|
};
|
|
process.on('exit', cleanup);
|
|
process.on('SIGINT', () => {
|
|
cleanup();
|
|
process.exit(130);
|
|
});
|
|
|
|
const ready = await waitForPortal(globalThis.fetch, startupTimeoutMs);
|
|
if (!ready) {
|
|
cleanup();
|
|
throw new Error(`isolated Portal did not start on :${portalPort} within ${startupTimeoutMs}ms\n${logTail}`);
|
|
}
|
|
|
|
const runtime = await requestJson(globalThis.fetch, `${baseUrl}/api/runtime/status`);
|
|
const targetStatuses = runtime.json?.targets ?? [];
|
|
const targets = Array.isArray(targetStatuses)
|
|
? targetStatuses.map((item) => item.target ?? item).filter(Boolean)
|
|
: [];
|
|
const primaryFromLog = logTail.includes('Goose v1.49 canary mode=all');
|
|
const hasV149 = targets.some((item) => String(item).includes(':18049')) || primaryFromLog;
|
|
|
|
if (!runtime.ok) {
|
|
cleanup();
|
|
throw new Error(`runtime status failed: ${runtime.status} ${runtime.text?.slice?.(0, 300)}`);
|
|
}
|
|
|
|
const memory = runtime.json?.memory ?? null;
|
|
if (!hasV149) {
|
|
cleanup();
|
|
throw new Error(`canary Portal must route to :18049, targets=${JSON.stringify(targets)}`);
|
|
}
|
|
|
|
if (memory?.backend !== 'legacy') {
|
|
const configSource = memory?.configSource ?? 'unknown';
|
|
if (configSource === 'admin-db' || configSource === 'admin-db-cache') {
|
|
console.log(
|
|
`GOOSE_V149_CANARY_PORTAL_WARN: memory.backend=${memory.backend} from ${configSource}; goose v1.49 routing verified`,
|
|
);
|
|
} else {
|
|
cleanup();
|
|
throw new Error(`canary Portal must use memory.backend=legacy, got ${memory?.backend} (${configSource})`);
|
|
}
|
|
}
|
|
|
|
const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
|
const user = {
|
|
username: `gv149c_${suffix}`,
|
|
password: 'GooseV149-Canary-2026',
|
|
email: `gv149c-${suffix}@example.test`,
|
|
displayName: `gv149c_${suffix}`,
|
|
};
|
|
|
|
const register = await requestJson(globalThis.fetch, `${baseUrl}/auth/register`, {
|
|
method: 'POST',
|
|
body: user,
|
|
});
|
|
if (!register.ok) {
|
|
cleanup();
|
|
throw new Error(`register failed: ${register.status}`);
|
|
}
|
|
|
|
const login = await requestJson(globalThis.fetch, `${baseUrl}/auth/login`, {
|
|
method: 'POST',
|
|
body: { username: user.username, password: user.password },
|
|
});
|
|
const cookie = login.headers?.get?.('set-cookie')?.split(';', 1)[0] ?? null;
|
|
if (!login.ok || !cookie) {
|
|
cleanup();
|
|
throw new Error(`login failed: ${login.status}`);
|
|
}
|
|
|
|
const started = await requestJson(globalThis.fetch, `${baseUrl}/api/agent/start`, {
|
|
method: 'POST',
|
|
headers: { Cookie: cookie },
|
|
body: {},
|
|
});
|
|
const sessionId = started.json?.id ?? null;
|
|
if (!started.ok || !sessionId) {
|
|
cleanup();
|
|
throw new Error(`agent/start failed: ${started.status} ${started.text?.slice?.(0, 200)}`);
|
|
}
|
|
|
|
const requestId = crypto.randomUUID();
|
|
const run = await requestJson(globalThis.fetch, `${baseUrl}/api/agent/runs`, {
|
|
method: 'POST',
|
|
headers: { Cookie: cookie },
|
|
body: {
|
|
session_id: sessionId,
|
|
request_id: requestId,
|
|
user_message: {
|
|
role: 'user',
|
|
created: Math.floor(Date.now() / 1000),
|
|
content: [{ type: 'text', text: 'canary ping' }],
|
|
metadata: { userVisible: true, agentVisible: true, displayText: 'canary ping' },
|
|
},
|
|
},
|
|
});
|
|
|
|
cleanup();
|
|
await sleep(300);
|
|
|
|
if (!run.ok || run.status !== 202) {
|
|
throw new Error(`agent run failed: ${run.status} ${run.text?.slice?.(0, 200)}`);
|
|
}
|
|
|
|
console.log(`GOOSE_V149_CANARY_PORTAL_OK: isolated Portal :${portalPort} session=${sessionId}`);
|
|
console.log(` memory.backend=${memory.backend} canaryPrimary=${canary.primary} targets=${targets.join(',') || 'log-confirmed'}`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`GOOSE_V149_CANARY_PORTAL_FAIL: ${error.message}`);
|
|
process.exit(1);
|
|
});
|