/** * 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(); }