mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-26 14:15:10 +00:00
* refactor: remove stale file-backed shims * fix: harden sqlite state ci boundaries * refactor: store matrix idb snapshots in sqlite * fix: satisfy rebased CI guardrails * refactor: store current conversation bindings in sqlite table * refactor: store tui last sessions in sqlite table * refactor: reset sqlite schema history * refactor: drop unshipped sqlite table migration * refactor: remove plugin index file rollback * refactor: drop unshipped sqlite sidecar migrations * refactor: remove runtime commitments kv migration * refactor: preserve kysely sync result types * refactor: drop unshipped sqlite schema migration table * test: keep session usage coverage sqlite-backed * refactor: keep sqlite migration doctor-only * refactor: isolate device legacy imports * refactor: isolate push voicewake legacy imports * refactor: isolate remaining runtime legacy imports * refactor: tighten sqlite migration guardrails * test: cover sqlite persisted enum parsing * refactor: isolate legacy update and tui imports * refactor: tighten sqlite state ownership * refactor: move legacy imports behind doctor * refactor: remove legacy session row lookup * refactor: canonicalize memory transcript locators * refactor: drop transcript path scope fallbacks * refactor: drop runtime legacy session delivery pruning * refactor: store tts prefs only in sqlite * refactor: remove cron store path runtime * refactor: use cron sqlite store keys * refactor: rename telegram message cache scope * refactor: read memory dreaming status from sqlite * refactor: rename cron status store key * refactor: stop remembering transcript file paths * test: use sqlite locators in agent fixtures * refactor: remove file-shaped commitments and cron store surfaces * refactor: keep compaction transcript handles out of session rows * refactor: derive transcript handles from session identity * refactor: derive runtime transcript handles * refactor: remove gateway session locator reads * refactor: remove transcript locator from session rows * refactor: store raw stream diagnostics in sqlite * refactor: remove file-shaped transcript rotation * refactor: hide legacy trajectory paths from runtime * refactor: remove runtime transcript file bridges * refactor: repair database-first rebase fallout * refactor: align tests with database-first state * refactor: remove transcript file handoffs * refactor: sync post-compaction memory by transcript scope * refactor: run codex app-server sessions by id * refactor: bind codex runtime state by session id * refactor: pass memory transcripts by sqlite scope * refactor: remove transcript locator cleanup leftovers * test: remove stale transcript file fixtures * refactor: remove transcript locator test helper * test: make cron sqlite keys explicit * test: remove cron runtime store paths * test: remove stale session file fixtures * test: use sqlite cron keys in diagnostics * refactor: remove runtime delivery queue backfill * test: drop fake export session file mocks * refactor: rename acp session read failure flag * refactor: rename acp row session key * refactor: remove session store test seams * refactor: move legacy session parser tests to doctor * refactor: reindex managed memory in place * refactor: drop stale session store wording * refactor: rename session row helpers * refactor: rename sqlite session entry modules * refactor: remove transcript locator leftovers * refactor: trim file-era audit wording * refactor: clean managed media through sqlite * fix: prefer explicit agent for exports * fix: use prepared agent for session resets * fix: canonicalize legacy codex binding import * test: rename state cleanup helper * docs: align backup docs with sqlite state * refactor: drop legacy Pi usage auth fallback * refactor: move legacy auth profile imports to doctor * refactor: keep Pi model discovery auth in memory * refactor: remove MSTeams legacy learning key fallback * refactor: store model catalog config in sqlite * refactor: use sqlite model catalog at runtime * refactor: remove model json compatibility aliases * refactor: store auth profiles in sqlite * refactor: seed copied auth profiles in sqlite * refactor: make auth profile runtime sqlite-addressed * refactor: migrate hermes secrets into sqlite auth store * refactor: move plugin install config migration to doctor * refactor: rename plugin index audit checks * test: drop auth file assumptions * test: remove legacy transcript file assertions * refactor: drop legacy cli session aliases * refactor: store skill uploads in sqlite * refactor: keep subagent attachments in sqlite vfs * refactor: drop subagent attachment cleanup state * refactor: move legacy session aliases to doctor * refactor: require node 24 for sqlite state runtime * refactor: move provider caches into sqlite state * fix: harden virtual agent filesystem * refactor: enforce database-first runtime state * refactor: rename compaction transcript rotation setting * test: clean sqlite refactor test types * refactor: consolidate sqlite runtime state * refactor: model session conversations in sqlite * refactor: stop deriving cron delivery from session keys * refactor: stop classifying sessions from key shape * refactor: hydrate announce targets from typed delivery * refactor: route heartbeat delivery from typed sqlite context * refactor: tighten typed sqlite session routing * refactor: remove session origin routing shadow * refactor: drop session origin shadow fixtures * perf: query sqlite vfs paths by prefix * refactor: use typed conversation metadata for sessions * refactor: prefer typed session routing metadata * refactor: require typed session routing metadata * refactor: resolve group tool policy from typed sessions * refactor: delete dead session thread info bridge * Show Codex subscription reset times in channel errors (#80456) * feat(plugin-sdk): consolidate session workflow APIs * fix(agents): allow read-only agent mount reads * [codex] refresh plugin regression fixtures * fix(agents): restore compaction gateway logs * test: tighten gateway startup assertions * Redact persisted secret-shaped payloads [AI] (#79006) * test: tighten device pair notify assertions * test: tighten hermes secret assertions * test: assert matrix client error shapes * test: assert config compat warnings * fix(heartbeat): remap cron-run exec events to session keys (#80214) * fix(codex): route btw through native side threads * fix(auth): accept friendly OpenAI order for Codex profiles * fix(codex): rotate auth profiles inside harness * fix: keep browser status page probe within timeout * test: assert agents add outputs * test: pin cron read status * fix(agents): avoid Pi resource discovery stalls Co-authored-by: dataCenter430 <titan032000@gmail.com> * fix: retire timed-out codex app-server clients * test: tighten qa lab runtime assertions * test: check security fix outputs * test: verify extension runtime messages * feat(wake): expose typed sessionKey on wake protocol + system event CLI * fix(gateway): await session_end during shutdown drain and track channel + compaction lifecycle paths (#57790) * test: guard talk consult call helper * fix(codex): scale context engine projection (#80761) * fix(codex): scale context engine projection * fix: document Codex context projection scaling * fix: document Codex context projection scaling * fix: document Codex context projection scaling * fix: document Codex context projection scaling * chore: align Codex projection changelog * chore: realign Codex projection changelog * fix: isolate Codex projection patch --------- Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org> Co-authored-by: Josh Lehman <josh@martian.engineering> * refactor: move agent runtime state toward piless * refactor: remove cron session reaper * refactor: move session management to sqlite * refactor: finish database-first state migration * chore: refresh generated sqlite db types * refactor: remove stale file-backed shims * test: harden kysely type coverage # Conflicts: # .agents/skills/kysely-database-access/SKILL.md # src/infra/kysely-sync.types.test.ts # src/proxy-capture/store.sqlite.test.ts # src/state/openclaw-agent-db.test.ts # src/state/openclaw-state-db.test.ts * refactor: remove cron store path runtime * refactor: keep compaction transcript handles out of session rows * refactor: derive embedded transcripts from sqlite identity * refactor: remove embedded transcript locator handoff * refactor: remove runtime transcript file bridges * refactor: remove transcript file handoffs * refactor: remove MSTeams legacy learning key fallback * refactor: store model catalog config in sqlite * refactor: use sqlite model catalog at runtime # Conflicts: # docs/cli/secrets.md # docs/gateway/authentication.md # docs/gateway/secrets.md * fix: keep oauth sibling sync sqlite-local # Conflicts: # src/commands/onboard-auth.test.ts * refactor: remove task session store maintenance # Conflicts: # src/commands/tasks.ts * refactor: keep diagnostics in state sqlite * refactor: enforce database-first runtime state * refactor: consolidate sqlite runtime state * Show Codex subscription reset times in channel errors (#80456) * fix(codex): refresh subscription limit resets * fix(codex): format reset times for channels * Update CHANGELOG with latest changes and fixes Updated CHANGELOG with recent fixes and improvements. * fix(codex): keep command load failures on codex surface * fix(codex): format account rate limits as rows * fix(codex): summarize account limits as usage status * fix(codex): simplify account limit status * test: tighten subagent announce queue assertion * test: tighten session delete lifecycle assertions * test: tighten cron ops assertions * fix: track cron execution milestones * test: tighten hermes secret assertions * test: assert matrix sync store payloads * test: assert config compat warnings * fix(codex): align btw side thread semantics * fix(codex): honor codex fallback blocking * fix(agents): avoid Pi resource discovery stalls * test: tighten codex event assertions * test: tighten cron assertions * Fix Codex app-server OAuth harness auth * refactor: move agent runtime state toward piless * refactor: move device and push state to sqlite * refactor: move runtime json state imports to doctor * refactor: finish database-first state migration * chore: refresh generated sqlite db types * refactor: clarify cron sqlite store keys * refactor: remove stale file-backed shims * refactor: bind codex runtime state by session id * test: expect sqlite trajectory branch export * refactor: rename session row helpers * fix: keep legacy device identity import in doctor * refactor: enforce database-first runtime state * refactor: consolidate sqlite runtime state * build: align pi contract wrappers * chore: repair database-first rebase * refactor: remove session file test contracts * test: update gateway session expectations * refactor: stop routing from session compatibility shadows * refactor: stop persisting session route shadows * refactor: use typed delivery context in clients * refactor: stop echoing session route shadows * refactor: repair embedded runner rebase imports # Conflicts: # src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts * refactor: align pi contract imports * refactor: satisfy kysely sync helper guard * refactor: remove file transcript bridge remnants * refactor: remove session locator compatibility * refactor: remove session file test contracts * refactor: keep rebase database-first clean * refactor: remove session file assumptions from e2e * docs: clarify database-first goal state * test: remove legacy store markers from sqlite runtime tests * refactor: remove legacy store assumptions from runtime seams * refactor: align sqlite runtime helper seams * test: update memory recall sqlite audit mock * refactor: align database-first runtime type seams * test: clarify doctor cron legacy store names * fix: preserve sqlite session route projections * test: fix copilot token cache test syntax * docs: update database-first proof status * test: align database-first test fixtures * docs: update database-first proof status * refactor: clean extension database-first drift * test: align agent session route proof * test: clarify doctor legacy path fixtures * chore: clean database-first changed checks * chore: repair database-first rebase markers * build: allow baileys git subdependency * chore: repair exp-vfs rebase drift * chore: finish exp-vfs rebase cleanup * chore: satisfy rebase lint drift * chore: fix qqbot rebase type seam * chore: fix rebase drift leftovers * fix: keep auth profile oauth secrets out of sqlite * fix: repair rebase drift tests * test: stabilize pairing request ordering * test: use source manifests in plugin contract checks * fix: restore gateway session metadata after rebase * fix: repair database-first rebase drift * fix: clean up database-first rebase fallout * test: stabilize line quick reply receipt time * fix: repair extension rebase drift * test: keep transcript redaction tests sqlite-backed * fix: carry injected transcript redaction through sqlite * chore: clean database branch rebase residue * fix: repair database branch CI drift * fix: repair database branch CI guard drift * fix: stabilize oauth tls preflight test * test: align database branch fast guards * test: repair build artifact boundary guards * chore: clean changelog rebase markers --------- Co-authored-by: pashpashpash <nik@vault77.ai> Co-authored-by: Eva <eva@100yen.org> Co-authored-by: stainlu <stainlu@newtype-ai.org> Co-authored-by: Jason Zhou <jason.zhou.design@gmail.com> Co-authored-by: Ruben Cuevas <hi@rubencu.com> Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com> Co-authored-by: Shakker <shakkerdroid@gmail.com> Co-authored-by: Kaspre <36520309+Kaspre@users.noreply.github.com> Co-authored-by: dataCenter430 <titan032000@gmail.com> Co-authored-by: Kaspre <kaspre@gmail.com> Co-authored-by: pandadev66 <nova.full.stack@outlook.com> Co-authored-by: Eva <admin@100yen.org> Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org> Co-authored-by: Josh Lehman <josh@martian.engineering> Co-authored-by: jeffjhunter <support@aipersonamethod.com>
447 lines
14 KiB
TypeScript
447 lines
14 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import JSON5 from "json5";
|
|
import {
|
|
loadPersistedAuthProfileStore,
|
|
savePersistedAuthProfileSecretsStore,
|
|
} from "../src/agents/auth-profiles/persisted.js";
|
|
|
|
type RestoreEntry = { key: string; value: string | undefined };
|
|
|
|
const LIVE_EXTERNAL_AUTH_DIRS = [".claude/backups", ".gemini", ".minimax"] as const;
|
|
const LIVE_EXTERNAL_AUTH_FILES = [
|
|
".claude.json",
|
|
".claude/.credentials.json",
|
|
".claude/settings.json",
|
|
".claude/settings.local.json",
|
|
".codex/auth.json",
|
|
".codex/config.toml",
|
|
] as const;
|
|
|
|
function isTruthyEnvValue(value: string | undefined): boolean {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
switch (value.trim().toLowerCase()) {
|
|
case "":
|
|
case "0":
|
|
case "false":
|
|
case "no":
|
|
case "off":
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function restoreEnv(entries: RestoreEntry[]): void {
|
|
for (const { key, value } of entries) {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveHomeRelativePath(input: string, homeDir: string): string {
|
|
const trimmed = input.trim();
|
|
if (trimmed === "~") {
|
|
return homeDir;
|
|
}
|
|
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
|
return path.join(homeDir, trimmed.slice(2));
|
|
}
|
|
return path.resolve(trimmed);
|
|
}
|
|
|
|
function loadProfileEnv(homeDir = os.homedir()): void {
|
|
const profilePath = path.join(homeDir, ".profile");
|
|
if (!fs.existsSync(profilePath)) {
|
|
return;
|
|
}
|
|
const applyEntry = (entry: string) => {
|
|
const idx = entry.indexOf("=");
|
|
if (idx <= 0) {
|
|
return false;
|
|
}
|
|
const key = entry.slice(0, idx).trim();
|
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) || (process.env[key] ?? "") !== "") {
|
|
return false;
|
|
}
|
|
process.env[key] = entry.slice(idx + 1);
|
|
return true;
|
|
};
|
|
const countAppliedEntries = (entries: Iterable<string>) => {
|
|
let applied = 0;
|
|
for (const entry of entries) {
|
|
if (applyEntry(entry)) {
|
|
applied += 1;
|
|
}
|
|
}
|
|
return applied;
|
|
};
|
|
try {
|
|
const output = execFileSync(
|
|
"/bin/bash",
|
|
["-lc", `set -a; source "${profilePath}" >/dev/null 2>&1; env -0`],
|
|
{ encoding: "utf8" },
|
|
);
|
|
const applied = countAppliedEntries(output.split("\0").filter(Boolean));
|
|
if (applied > 0 && !isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST_QUIET)) {
|
|
console.log(`[live] loaded ${applied} env vars from ~/.profile`);
|
|
}
|
|
} catch {
|
|
try {
|
|
const fallbackEntries = fs
|
|
.readFileSync(profilePath, "utf8")
|
|
.split(/\r?\n/u)
|
|
.map((line) => line.trim())
|
|
.filter((line) => line && !line.startsWith("#"))
|
|
.map((line) => line.replace(/^export\s+/u, ""))
|
|
.map((line) => {
|
|
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u);
|
|
if (!match) {
|
|
return "";
|
|
}
|
|
let value = match[2].trim();
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
(value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
return `${match[1]}=${value}`;
|
|
})
|
|
.filter(Boolean);
|
|
const applied = countAppliedEntries(fallbackEntries);
|
|
if (applied > 0 && !isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST_QUIET)) {
|
|
console.log(`[live] loaded ${applied} env vars from ~/.profile`);
|
|
}
|
|
} catch {
|
|
// ignore profile load failures
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveRestoreEntries(): RestoreEntry[] {
|
|
return [
|
|
{ key: "OPENCLAW_TEST_FAST", value: process.env.OPENCLAW_TEST_FAST },
|
|
{
|
|
key: "OPENCLAW_STRICT_FAST_REPLY_CONFIG",
|
|
value: process.env.OPENCLAW_STRICT_FAST_REPLY_CONFIG,
|
|
},
|
|
{
|
|
key: "OPENCLAW_ALLOW_SLOW_REPLY_TESTS",
|
|
value: process.env.OPENCLAW_ALLOW_SLOW_REPLY_TESTS,
|
|
},
|
|
{ key: "HOME", value: process.env.HOME },
|
|
{ key: "USERPROFILE", value: process.env.USERPROFILE },
|
|
{ key: "XDG_CONFIG_HOME", value: process.env.XDG_CONFIG_HOME },
|
|
{ key: "XDG_DATA_HOME", value: process.env.XDG_DATA_HOME },
|
|
{ key: "XDG_STATE_HOME", value: process.env.XDG_STATE_HOME },
|
|
{ key: "XDG_CACHE_HOME", value: process.env.XDG_CACHE_HOME },
|
|
{ key: "OPENCLAW_STATE_DIR", value: process.env.OPENCLAW_STATE_DIR },
|
|
{ key: "OPENCLAW_CONFIG_PATH", value: process.env.OPENCLAW_CONFIG_PATH },
|
|
{ key: "OPENCLAW_GATEWAY_PORT", value: process.env.OPENCLAW_GATEWAY_PORT },
|
|
{ key: "OPENCLAW_BRIDGE_ENABLED", value: process.env.OPENCLAW_BRIDGE_ENABLED },
|
|
{ key: "OPENCLAW_BRIDGE_HOST", value: process.env.OPENCLAW_BRIDGE_HOST },
|
|
{ key: "OPENCLAW_BRIDGE_PORT", value: process.env.OPENCLAW_BRIDGE_PORT },
|
|
{ key: "OPENCLAW_CANVAS_HOST_PORT", value: process.env.OPENCLAW_CANVAS_HOST_PORT },
|
|
{ key: "OPENCLAW_TEST_HOME", value: process.env.OPENCLAW_TEST_HOME },
|
|
{ key: "OPENCLAW_AGENT_DIR", value: process.env.OPENCLAW_AGENT_DIR },
|
|
{ key: "PI_CODING_AGENT_DIR", value: process.env.PI_CODING_AGENT_DIR },
|
|
{ key: "TELEGRAM_BOT_TOKEN", value: process.env.TELEGRAM_BOT_TOKEN },
|
|
{ key: "DISCORD_BOT_TOKEN", value: process.env.DISCORD_BOT_TOKEN },
|
|
{ key: "SLACK_BOT_TOKEN", value: process.env.SLACK_BOT_TOKEN },
|
|
{ key: "SLACK_APP_TOKEN", value: process.env.SLACK_APP_TOKEN },
|
|
{ key: "SLACK_USER_TOKEN", value: process.env.SLACK_USER_TOKEN },
|
|
{ key: "COPILOT_GITHUB_TOKEN", value: process.env.COPILOT_GITHUB_TOKEN },
|
|
{ key: "GH_TOKEN", value: process.env.GH_TOKEN },
|
|
{ key: "GITHUB_TOKEN", value: process.env.GITHUB_TOKEN },
|
|
{ key: "NODE_OPTIONS", value: process.env.NODE_OPTIONS },
|
|
];
|
|
}
|
|
|
|
function createIsolatedTestHome(restore: RestoreEntry[]): {
|
|
cleanup: () => void;
|
|
tempHome: string;
|
|
} {
|
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-test-home-"));
|
|
|
|
process.env.HOME = tempHome;
|
|
process.env.USERPROFILE = tempHome;
|
|
process.env.OPENCLAW_TEST_HOME = tempHome;
|
|
process.env.OPENCLAW_TEST_FAST = "1";
|
|
process.env.OPENCLAW_STRICT_FAST_REPLY_CONFIG = "1";
|
|
delete process.env.OPENCLAW_ALLOW_SLOW_REPLY_TESTS;
|
|
|
|
// Ensure test runs never touch the developer's real config/state, even if they have overrides set.
|
|
delete process.env.OPENCLAW_CONFIG_PATH;
|
|
// Prefer deriving state dir from HOME so nested tests that change HOME also isolate correctly.
|
|
delete process.env.OPENCLAW_STATE_DIR;
|
|
delete process.env.OPENCLAW_AGENT_DIR;
|
|
delete process.env.PI_CODING_AGENT_DIR;
|
|
// Prefer test-controlled ports over developer overrides (avoid port collisions across tests/workers).
|
|
delete process.env.OPENCLAW_GATEWAY_PORT;
|
|
delete process.env.OPENCLAW_BRIDGE_ENABLED;
|
|
delete process.env.OPENCLAW_BRIDGE_HOST;
|
|
delete process.env.OPENCLAW_BRIDGE_PORT;
|
|
delete process.env.OPENCLAW_CANVAS_HOST_PORT;
|
|
// Avoid leaking real GitHub/Copilot tokens into non-live test runs.
|
|
delete process.env.TELEGRAM_BOT_TOKEN;
|
|
delete process.env.DISCORD_BOT_TOKEN;
|
|
delete process.env.SLACK_BOT_TOKEN;
|
|
delete process.env.SLACK_APP_TOKEN;
|
|
delete process.env.SLACK_USER_TOKEN;
|
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
|
delete process.env.GH_TOKEN;
|
|
delete process.env.GITHUB_TOKEN;
|
|
// Avoid leaking local dev tooling flags into tests (e.g. --inspect).
|
|
delete process.env.NODE_OPTIONS;
|
|
|
|
// Windows: prefer the default state dir so auth/profile tests match real paths.
|
|
if (process.platform === "win32") {
|
|
process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw");
|
|
}
|
|
|
|
process.env.XDG_CONFIG_HOME = path.join(tempHome, ".config");
|
|
process.env.XDG_DATA_HOME = path.join(tempHome, ".local", "share");
|
|
process.env.XDG_STATE_HOME = path.join(tempHome, ".local", "state");
|
|
process.env.XDG_CACHE_HOME = path.join(tempHome, ".cache");
|
|
|
|
const cleanup = () => {
|
|
restoreEnv(restore);
|
|
try {
|
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
} catch {
|
|
// ignore cleanup errors
|
|
}
|
|
};
|
|
|
|
return { cleanup, tempHome };
|
|
}
|
|
|
|
function ensureParentDir(targetPath: string): void {
|
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
}
|
|
|
|
function copyDirIfExists(sourcePath: string, targetPath: string): void {
|
|
if (!fs.existsSync(sourcePath)) {
|
|
return;
|
|
}
|
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
fs.cpSync(sourcePath, targetPath, {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|
|
|
|
function copyFileIfExists(sourcePath: string, targetPath: string): void {
|
|
if (!fs.existsSync(sourcePath)) {
|
|
return;
|
|
}
|
|
let stat: fs.Stats;
|
|
try {
|
|
stat = fs.statSync(sourcePath);
|
|
} catch {
|
|
return;
|
|
}
|
|
if (!stat.isFile()) {
|
|
return;
|
|
}
|
|
ensureParentDir(targetPath);
|
|
fs.copyFileSync(sourcePath, targetPath);
|
|
}
|
|
|
|
function restoreClaudeConfigFromBackupIfNeeded(tempHome: string): void {
|
|
const targetPath = path.join(tempHome, ".claude.json");
|
|
if (fs.existsSync(targetPath)) {
|
|
return;
|
|
}
|
|
const backupsDir = path.join(tempHome, ".claude", "backups");
|
|
if (!fs.existsSync(backupsDir)) {
|
|
return;
|
|
}
|
|
const latestBackup = fs
|
|
.readdirSync(backupsDir)
|
|
.filter((entry) => entry.startsWith(".claude.json.backup."))
|
|
.toSorted()
|
|
.at(-1);
|
|
if (!latestBackup) {
|
|
return;
|
|
}
|
|
copyFileIfExists(path.join(backupsDir, latestBackup), targetPath);
|
|
}
|
|
|
|
function sanitizeLiveConfig(raw: string): string {
|
|
try {
|
|
const parsed: {
|
|
agents?: {
|
|
defaults?: Record<string, unknown>;
|
|
list?: Array<Record<string, unknown>>;
|
|
};
|
|
} = JSON5.parse(raw);
|
|
|
|
if (!parsed || typeof parsed !== "object") {
|
|
return raw;
|
|
}
|
|
|
|
if (parsed.agents?.defaults && typeof parsed.agents.defaults === "object") {
|
|
delete parsed.agents.defaults.workspace;
|
|
delete parsed.agents.defaults.agentDir;
|
|
}
|
|
|
|
if (Array.isArray(parsed.agents?.list)) {
|
|
parsed.agents.list = parsed.agents.list.map((entry) => {
|
|
if (!entry || typeof entry !== "object") {
|
|
return entry;
|
|
}
|
|
const nextEntry = { ...entry };
|
|
delete nextEntry.workspace;
|
|
delete nextEntry.agentDir;
|
|
return nextEntry;
|
|
});
|
|
}
|
|
|
|
return `${JSON.stringify(parsed, null, 2)}\n`;
|
|
} catch {
|
|
return raw;
|
|
}
|
|
}
|
|
|
|
function stageLiveAuthProfiles(params: {
|
|
env: NodeJS.ProcessEnv;
|
|
realHome: string;
|
|
realStateDir: string;
|
|
tempHome: string;
|
|
tempStateDir: string;
|
|
}): void {
|
|
const agentsDir = path.join(params.realStateDir, "agents");
|
|
if (!fs.existsSync(agentsDir)) {
|
|
return;
|
|
}
|
|
const sourceEnv: NodeJS.ProcessEnv = {
|
|
...params.env,
|
|
HOME: params.realHome,
|
|
USERPROFILE: params.realHome,
|
|
OPENCLAW_STATE_DIR: params.realStateDir,
|
|
};
|
|
const targetEnv: NodeJS.ProcessEnv = {
|
|
...params.env,
|
|
HOME: params.tempHome,
|
|
USERPROFILE: params.tempHome,
|
|
OPENCLAW_STATE_DIR: params.tempStateDir,
|
|
};
|
|
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
const sourceAgentDir = path.join(agentsDir, entry.name, "agent");
|
|
const store = loadPersistedAuthProfileStore(sourceAgentDir, { env: sourceEnv });
|
|
if (!store) {
|
|
continue;
|
|
}
|
|
const targetAgentDir = path.join(params.tempStateDir, "agents", entry.name, "agent");
|
|
fs.mkdirSync(targetAgentDir, { recursive: true });
|
|
savePersistedAuthProfileSecretsStore(store, targetAgentDir, { env: targetEnv });
|
|
}
|
|
}
|
|
|
|
function stageLiveTestState(params: {
|
|
env: NodeJS.ProcessEnv;
|
|
realHome: string;
|
|
tempHome: string;
|
|
}): void {
|
|
const rawStateDir = params.env.OPENCLAW_STATE_DIR?.trim();
|
|
let realStateDir = rawStateDir
|
|
? resolveHomeRelativePath(rawStateDir, params.realHome)
|
|
: path.join(params.realHome, ".openclaw");
|
|
const priorIsolatedHome = params.env.OPENCLAW_TEST_HOME?.trim();
|
|
const snapshotHome = params.env.HOME?.trim();
|
|
if (
|
|
priorIsolatedHome &&
|
|
snapshotHome &&
|
|
snapshotHome !== priorIsolatedHome &&
|
|
realStateDir === path.join(priorIsolatedHome, ".openclaw")
|
|
) {
|
|
realStateDir = path.join(params.realHome, ".openclaw");
|
|
}
|
|
const tempStateDir = path.join(params.tempHome, ".openclaw");
|
|
fs.mkdirSync(tempStateDir, { recursive: true });
|
|
fs.mkdirSync(path.join(params.tempHome, ".gemini"), { recursive: true });
|
|
|
|
const realConfigPath = params.env.OPENCLAW_CONFIG_PATH?.trim()
|
|
? resolveHomeRelativePath(params.env.OPENCLAW_CONFIG_PATH, params.realHome)
|
|
: path.join(realStateDir, "openclaw.json");
|
|
if (fs.existsSync(realConfigPath)) {
|
|
const rawConfig = fs.readFileSync(realConfigPath, "utf8");
|
|
fs.writeFileSync(
|
|
path.join(tempStateDir, "openclaw.json"),
|
|
sanitizeLiveConfig(rawConfig),
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
copyDirIfExists(path.join(realStateDir, "credentials"), path.join(tempStateDir, "credentials"));
|
|
copyDirIfExists(
|
|
path.join(realStateDir, "external-plugins"),
|
|
path.join(tempStateDir, "external-plugins"),
|
|
);
|
|
stageLiveAuthProfiles({
|
|
env: params.env,
|
|
realHome: params.realHome,
|
|
realStateDir,
|
|
tempHome: params.tempHome,
|
|
tempStateDir,
|
|
});
|
|
|
|
for (const authDir of LIVE_EXTERNAL_AUTH_DIRS) {
|
|
copyDirIfExists(path.join(params.realHome, authDir), path.join(params.tempHome, authDir));
|
|
}
|
|
for (const authFile of LIVE_EXTERNAL_AUTH_FILES) {
|
|
copyFileIfExists(path.join(params.realHome, authFile), path.join(params.tempHome, authFile));
|
|
}
|
|
restoreClaudeConfigFromBackupIfNeeded(params.tempHome);
|
|
}
|
|
|
|
export function installTestEnv(options?: { loadProfileEnv?: boolean }): {
|
|
cleanup: () => void;
|
|
tempHome: string;
|
|
} {
|
|
const live =
|
|
process.env.LIVE === "1" ||
|
|
process.env.OPENCLAW_LIVE_TEST === "1" ||
|
|
process.env.OPENCLAW_LIVE_GATEWAY === "1";
|
|
const allowRealHome = isTruthyEnvValue(process.env.OPENCLAW_LIVE_USE_REAL_HOME);
|
|
const realHome = process.env.HOME ?? os.homedir();
|
|
const liveEnvSnapshot = { ...process.env };
|
|
|
|
const shouldLoadProfileEnv = options?.loadProfileEnv ?? (live || allowRealHome);
|
|
if (shouldLoadProfileEnv) {
|
|
loadProfileEnv(realHome);
|
|
}
|
|
|
|
if (live && allowRealHome) {
|
|
return { cleanup: () => {}, tempHome: realHome };
|
|
}
|
|
|
|
const restore = resolveRestoreEntries();
|
|
const testEnv = createIsolatedTestHome(restore);
|
|
|
|
if (live) {
|
|
stageLiveTestState({ env: liveEnvSnapshot, realHome, tempHome: testEnv.tempHome });
|
|
}
|
|
|
|
return testEnv;
|
|
}
|
|
|
|
export function withIsolatedTestHome(options?: { loadProfileEnv?: boolean }): {
|
|
cleanup: () => void;
|
|
tempHome: string;
|
|
} {
|
|
return installTestEnv(options);
|
|
}
|