67220a14ea
Prevent phase2/phase3/all reruns from burning DashScope tokens unless GOOSE_V149_ALLOW_REAL_LLM=1 is explicitly set with human approval. Co-authored-by: Cursor <cursoragent@cursor.com>
251 lines
7.7 KiB
JavaScript
251 lines
7.7 KiB
JavaScript
/**
|
|
* Local Goose v1.49 canary routing helpers (loopback only).
|
|
* Production hosts are refused; see docs/goose-v149-canary-runbook.md.
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { createCanaryPolicy, resolveCanaryTarget } from '../release-gate/canary-routing.mjs';
|
|
import { applyRealLlmSkipDefaults } from './goose-v149-real-llm-gate.mjs';
|
|
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
|
|
|
|
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
function loadEnvFile(filePath) {
|
|
if (!fs.existsSync(filePath)) return;
|
|
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
const eq = trimmed.indexOf('=');
|
|
if (eq < 0) continue;
|
|
const key = trimmed.slice(0, eq).trim();
|
|
let value = trimmed.slice(eq + 1).trim();
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"'))
|
|
|| (value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
if (!(key in process.env)) process.env[key] = value;
|
|
}
|
|
}
|
|
|
|
loadEnvFile(path.join(memindRoot, '.env.local'));
|
|
|
|
export const GOOSE_V149_CANARY_BEGIN = '# GOOSE_V149_CANARY_BEGIN';
|
|
export const GOOSE_V149_CANARY_END = '# GOOSE_V149_CANARY_END';
|
|
|
|
/** Force-apply GOOSE_V149 canary block from .env.local (overrides .env). */
|
|
export function applyGooseV149CanaryBlockEnv(env = process.env, rootDir = memindRoot) {
|
|
const envFile = path.join(rootDir, '.env.local');
|
|
if (!fs.existsSync(envFile)) return;
|
|
const lines = fs.readFileSync(envFile, 'utf8').split('\n');
|
|
let inBlock = false;
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (trimmed === GOOSE_V149_CANARY_BEGIN) {
|
|
inBlock = true;
|
|
continue;
|
|
}
|
|
if (trimmed === GOOSE_V149_CANARY_END) break;
|
|
if (!inBlock || !trimmed || trimmed.startsWith('#')) continue;
|
|
const eq = trimmed.indexOf('=');
|
|
if (eq < 0) continue;
|
|
const key = trimmed.slice(0, eq).trim();
|
|
let value = trimmed.slice(eq + 1).trim();
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"'))
|
|
|| (value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
env[key] = value;
|
|
}
|
|
}
|
|
|
|
function applyLocalScenarioCredentials(env) {
|
|
if (!env.JOHN_PASSWORD && !env.H5_ACCESS_PASSWORD && !env.MEMIND_PASSWORD) {
|
|
env.JOHN_PASSWORD = '981122tj';
|
|
}
|
|
if (!env.RELEASE_GATE_SCENARIO_USERNAME) {
|
|
env.RELEASE_GATE_SCENARIO_USERNAME = 'john';
|
|
}
|
|
return env;
|
|
}
|
|
|
|
/** Load Memind env files then force-apply the GOOSE_V149 canary block (legacy memory, loopback targets). */
|
|
export function prepareGooseV149CheckEnv(env = process.env, rootDir = memindRoot) {
|
|
loadMemindEnvFiles(rootDir, env);
|
|
applyGooseV149CanaryBlockEnv(env, rootDir);
|
|
applyLocalScenarioCredentials(env);
|
|
applyRealLlmSkipDefaults(env);
|
|
const canary = resolveGooseApiTargetsFromEnv(env);
|
|
if (canary?.mode && canary.mode !== 'off' && env.GOOSE_V149_MEMORY_DRIFT_REFRESH_AFTER_SMOKE == null) {
|
|
env.GOOSE_V149_MEMORY_DRIFT_REFRESH_AFTER_SMOKE = '1';
|
|
}
|
|
return env;
|
|
}
|
|
|
|
export const GOOSE_CANARY_MODES = new Set(['off', 'all', 'users']);
|
|
|
|
const BLOCKED_HOSTS = new Set([
|
|
'58.38.22.103',
|
|
'120.26.184.105',
|
|
'103.tkmind.cn',
|
|
'105.tkmind.cn',
|
|
]);
|
|
|
|
export const DEFAULT_GOOSE_STABLE_TARGET = 'https://127.0.0.1:18006';
|
|
export const DEFAULT_GOOSE_V149_TARGET = 'https://127.0.0.1:18049';
|
|
|
|
function csvSet(value) {
|
|
return new Set(
|
|
String(value ?? '')
|
|
.split(',')
|
|
.map((item) => item.trim())
|
|
.filter(Boolean),
|
|
);
|
|
}
|
|
|
|
export function parseGooseCanaryMode(env = process.env) {
|
|
const mode = String(env.TKMIND_GOOSE_CANARY ?? 'off').trim().toLowerCase();
|
|
if (!GOOSE_CANARY_MODES.has(mode)) {
|
|
throw new Error(
|
|
`Unsupported TKMIND_GOOSE_CANARY "${mode}". Expected: ${[...GOOSE_CANARY_MODES].join(', ')}`,
|
|
);
|
|
}
|
|
return mode;
|
|
}
|
|
|
|
export function assertLoopbackGooseTarget(rawTarget, label = 'goose target') {
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(String(rawTarget ?? '').trim());
|
|
} catch {
|
|
throw new Error(`${label} must be a valid URL`);
|
|
}
|
|
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
throw new Error(`${label} must use http or https`);
|
|
}
|
|
const host = parsed.hostname.toLowerCase();
|
|
if (BLOCKED_HOSTS.has(host)) {
|
|
throw new Error(`${label} refuses production host ${host}`);
|
|
}
|
|
if (!['127.0.0.1', 'localhost', '::1'].includes(host)) {
|
|
throw new Error(`${label} must be loopback (127.0.0.1 / localhost / ::1)`);
|
|
}
|
|
parsed.pathname = parsed.pathname.replace(/\/+$/, '') || '';
|
|
parsed.search = '';
|
|
parsed.hash = '';
|
|
return parsed.toString().replace(/\/+$/, '');
|
|
}
|
|
|
|
export function gooseCanaryPolicyFromEnv(env = process.env) {
|
|
const userIds = csvSet(env.TKMIND_GOOSE_CANARY_USER_IDS);
|
|
const usernames = csvSet(env.TKMIND_GOOSE_CANARY_USERNAMES);
|
|
const wechatUserIds = csvSet(env.TKMIND_GOOSE_CANARY_WECHAT_USER_IDS);
|
|
if (!userIds.size && !usernames.size && !wechatUserIds.size) {
|
|
return null;
|
|
}
|
|
return createCanaryPolicy({ userIds, usernames, wechatUserIds, candidateTarget: 'candidate' });
|
|
}
|
|
|
|
/**
|
|
* Resolve Portal → goosed API targets for local v1.49 canary.
|
|
* Returns null when canary is off (caller keeps legacy TKMIND_API_TARGET parsing).
|
|
*/
|
|
export function resolveGooseApiTargetsFromEnv(env = process.env) {
|
|
const mode = parseGooseCanaryMode(env);
|
|
if (mode === 'off') return null;
|
|
|
|
const stable = assertLoopbackGooseTarget(
|
|
env.TKMIND_API_TARGET_STABLE ?? DEFAULT_GOOSE_STABLE_TARGET,
|
|
'TKMIND_API_TARGET_STABLE',
|
|
);
|
|
const v149 = assertLoopbackGooseTarget(
|
|
env.TKMIND_API_TARGET_V149 ?? DEFAULT_GOOSE_V149_TARGET,
|
|
'TKMIND_API_TARGET_V149',
|
|
);
|
|
|
|
if (mode === 'all') {
|
|
return Object.freeze({
|
|
mode,
|
|
stable,
|
|
v149,
|
|
primary: v149,
|
|
targets: [v149],
|
|
});
|
|
}
|
|
|
|
const policy = gooseCanaryPolicyFromEnv(env);
|
|
if (!policy) {
|
|
throw new Error(
|
|
'TKMIND_GOOSE_CANARY=users requires TKMIND_GOOSE_CANARY_USER_IDS and/or '
|
|
+ 'TKMIND_GOOSE_CANARY_USERNAMES / TKMIND_GOOSE_CANARY_WECHAT_USER_IDS',
|
|
);
|
|
}
|
|
|
|
return Object.freeze({
|
|
mode,
|
|
stable,
|
|
v149,
|
|
primary: stable,
|
|
targets: [stable, v149],
|
|
policy,
|
|
});
|
|
}
|
|
|
|
export function resolveGooseTargetForIdentity(identity, config) {
|
|
if (!config || config.mode === 'off') {
|
|
return config?.stable ?? DEFAULT_GOOSE_STABLE_TARGET;
|
|
}
|
|
if (config.mode === 'all') return config.v149;
|
|
const targetKey = resolveCanaryTarget(identity, config.policy);
|
|
return targetKey === 'candidate' ? config.v149 : config.stable;
|
|
}
|
|
|
|
export function describeGooseCanaryConfig(env = process.env) {
|
|
const config = resolveGooseApiTargetsFromEnv(env);
|
|
if (!config) {
|
|
return {
|
|
enabled: false,
|
|
mode: 'off',
|
|
stable: assertLoopbackGooseTarget(
|
|
env.TKMIND_API_TARGET ?? DEFAULT_GOOSE_STABLE_TARGET,
|
|
'TKMIND_API_TARGET',
|
|
),
|
|
v149: assertLoopbackGooseTarget(
|
|
env.TKMIND_API_TARGET_V149 ?? DEFAULT_GOOSE_V149_TARGET,
|
|
'TKMIND_API_TARGET_V149',
|
|
),
|
|
};
|
|
}
|
|
return {
|
|
enabled: true,
|
|
mode: config.mode,
|
|
stable: config.stable,
|
|
v149: config.v149,
|
|
primary: config.primary,
|
|
targets: [...config.targets],
|
|
policySelectors: config.policy
|
|
? {
|
|
userIds: [...config.policy.userIds],
|
|
usernames: [...config.policy.usernames],
|
|
wechatUserIds: [...config.policy.wechatUserIds],
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function printCliStatus() {
|
|
const summary = describeGooseCanaryConfig(process.env);
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
}
|
|
|
|
const modulePath = fileURLToPath(import.meta.url);
|
|
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
if (invokedPath && invokedPath === modulePath) {
|
|
printCliStatus();
|
|
}
|