fix(FN-XXXX): narrow backup matcher and isolate version probes
Two follow-up corrections to the in-process auto-backup interception: - The matcher previously hijacked any `fn backup …` / `fusion backup …` / `runfusion.ai backup …` form. The in-process replacement only knows how to do `--create` + cleanup, so scheduling `--list`, `--cleanup`, or `--restore <file>` would have silently executed a create instead of the requested operation. The matcher is now anchored to `backup --create` (with optional trailing flags), with positive/negative unit tests. - Step-based automations (`AutomationStep` with `type: "command"`) also shell out — the legacy-command interception alone left that path vulnerable. `executeCommandStep` now applies the same in-process backup detour, factored through a shared `runBackupActionInProcess` helper. Independently, `runProbe` in fn-binary now spawns with `cwd: tmpdir()`. The dashboard's `/system/fn-binary/status` route runs `<bin> --version` on whatever fusion binary happens to be on PATH — older releases (e.g. v0.13.0) initialise an engine and create a fresh `.fusion/<project>/ .fusion/` tree as a side effect. Pinning the probe's cwd to the OS temp directory keeps any such artefacts off the developer's project. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,19 +35,31 @@ function execCommand(command: string, options: Parameters<typeof exec>[1]): Prom
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognize commands that the auto-backup feature schedules. These shell out
|
||||
* to whatever fusion binary is on PATH — which may be older than the running
|
||||
* process and still carry the pluginStore-rootDir bug that creates a stray
|
||||
* `.fusion/.fusion/` directory. We intercept and run the backup in-process.
|
||||
* Recognize commands that the auto-backup feature schedules — specifically
|
||||
* the `backup --create` form that the engine itself writes via
|
||||
* `syncBackupAutomation`. Other backup subcommands (`--list`, `--cleanup`,
|
||||
* `--restore <file>`) are intentionally NOT intercepted because the
|
||||
* in-process replacement only performs a create+cleanup; intercepting them
|
||||
* would silently execute the wrong operation.
|
||||
*
|
||||
* These commands shell out to whatever fusion binary is on PATH, which may
|
||||
* be older than the running process and still carry the pluginStore-rootDir
|
||||
* bug that creates a stray `.fusion/.fusion/` directory. Intercepting the
|
||||
* `--create` form keeps the auto-backup self-contained inside the running
|
||||
* engine.
|
||||
*/
|
||||
export function isInProcessBackupCommand(command: string | undefined): boolean {
|
||||
if (!command) return false;
|
||||
const normalized = command.trim().toLowerCase();
|
||||
// Allow the binary name + the `backup --create` subcommand, optionally
|
||||
// followed by additional whitespace-separated flags. Reject any other
|
||||
// backup subcommand (--list, --cleanup, --restore, etc.).
|
||||
const tail = /\s+backup\s+--create(?:\s+.*)?$/;
|
||||
return (
|
||||
/^(?:npx\s+)?runfusion(?:\.ai)?\s+backup\b/.test(normalized) ||
|
||||
/^(?:npx\s+)?@runfusion\/fusion\s+backup\b/.test(normalized) ||
|
||||
/^fn\s+backup\b/.test(normalized) ||
|
||||
/^fusion\s+backup\b/.test(normalized)
|
||||
new RegExp(`^(?:npx\\s+)?runfusion(?:\\.ai)?${tail.source}`).test(normalized) ||
|
||||
new RegExp(`^(?:npx\\s+)?@runfusion\\/fusion${tail.source}`).test(normalized) ||
|
||||
new RegExp(`^fn${tail.source}`).test(normalized) ||
|
||||
new RegExp(`^fusion${tail.source}`).test(normalized)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -361,35 +373,44 @@ export class CronRunner {
|
||||
schedule: ScheduledTask,
|
||||
startedAt: string,
|
||||
): Promise<AutomationRunResult> {
|
||||
const action = await this.runBackupActionInProcess();
|
||||
if (action.success) {
|
||||
log.log(`✓ ${schedule.name} completed in-process`);
|
||||
} else {
|
||||
log.warn(`✗ ${schedule.name} in-process backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
|
||||
}
|
||||
return {
|
||||
success: action.success,
|
||||
output: action.output,
|
||||
error: action.error,
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared in-process backup execution used by both the legacy-command path
|
||||
* and the command-step path. Returns the success/output/error tuple in
|
||||
* a shape that callers can wrap into either a run or a step result.
|
||||
*/
|
||||
private async runBackupActionInProcess(): Promise<{
|
||||
success: boolean;
|
||||
output: string;
|
||||
error: string | undefined;
|
||||
}> {
|
||||
try {
|
||||
const { runBackupCommand } = await import("@fusion/core");
|
||||
const fusionDir = this.store.getFusionDir();
|
||||
const settings = await this.store.getSettings();
|
||||
const result = await runBackupCommand(fusionDir, settings);
|
||||
|
||||
if (result.success) {
|
||||
log.log(`✓ ${schedule.name} completed in-process`);
|
||||
} else {
|
||||
log.warn(`✗ ${schedule.name} in-process backup reported failure: ${result.output}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
output: truncateOutput(result.output ?? "", ""),
|
||||
error: result.success ? undefined : result.output,
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
log.warn(`✗ ${schedule.name} in-process backup threw: ${message}`);
|
||||
return {
|
||||
success: false,
|
||||
output: "",
|
||||
error: message,
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
return { success: false, output: "", error: message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,6 +532,23 @@ export class CronRunner {
|
||||
};
|
||||
}
|
||||
|
||||
// Step-based automations can also carry the auto-backup command. Mirror
|
||||
// the legacy-command interception so step-form schedules don't fall back
|
||||
// to spawning a stale `runfusion.ai` binary.
|
||||
if (isInProcessBackupCommand(step.command)) {
|
||||
const action = await this.runBackupActionInProcess();
|
||||
return {
|
||||
stepId: step.id,
|
||||
stepName: step.name,
|
||||
stepIndex,
|
||||
success: action.success,
|
||||
output: action.output,
|
||||
error: action.error,
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execCommand(step.command, {
|
||||
timeout: timeoutMs,
|
||||
|
||||
Reference in New Issue
Block a user