Files
openclaw/src/cli/gateway-rpc.runtime.ts
T
Peter Steinberger 37b4fc8621 feat(backup): recorded runs, scheduled backups, and git-backed versioned snapshots (#122485)
* refactor(infra): extract shared git exec and verified snapshot-copy helpers

Moves the worktrees git wrapper to src/infra/git-exec.ts (with optional
maxOutputBytes for large buffered reads) and the online-backup/sanitize/
VACUUM/verify snapshot step into src/snapshot/openclaw-snapshot-copy.ts so
snapshot backends share one hardened copy path. Behavior-identical moves;
all importers updated.

* feat(snapshot): git-backed versioned SQLite snapshot engine

Deterministic per-table JSONL dumps (PK-ordered, lossless bigint/blob
encoding), verbatim DDL preservation, virtual/shadow-table skipping with
FTS rebuild on restore, secret-table redaction policy, manifest with
per-table row counts and content hashes, and restore verification by
re-serialization. Unchanged data produces no commit.

* feat(backup): recorded runs, freshness surfacing, and scheduled git backups

Every backup attempt is recorded in the previously writer-less backup_runs
table (bounded to 200 rows). openclaw status gains a Backups overview row
and JSON payload; doctor prints an informational hint when no successful
backup is recorded or the newest is stale. New commands: backup git
init/create/log/verify/restore and backup enable/disable, which provision
one idempotent gateway cron job running scheduled git backups.

* fix(state): stop bumping schema_meta.updated_at on unchanged opens

updated_at now records when schema metadata actually changed instead of
when the database was last opened; unconditional bumps dirtied the row on
every open and defeated no-change backup detection.

* docs: document versioned git backups, scheduling, and backup freshness

* fix(backup): satisfy CI ownership checks

* fix(backup): complete CI contract coverage

* fix(backup): complete credential table redaction

* fix(backup): isolate git repository ownership

* fix(backup): persist push degradation

* fix(backup): atomically converge schedules

* fix(status): isolate backup freshness environment

* fix(status): carry scan environment to freshness reads

* fix(backup): harden Git repository ownership

* docs(backup): document Git repository safety

* fix(backup): non-creating outcome log and origin preflight for pushed schedules

Recording a backup outcome never bootstraps an absent state database (a
failed backup on a fresh host would otherwise create a blank DB that a
retry treats as real input), and backup enable --push now requires the
repository to have an origin remote, pointing at backup git init --remote
instead of scheduling permanently degraded pushes.

* refactor(worktrees): use shared git exec helpers

* refactor(worktrees): remove unused git buffer wrapper

* refactor(worktrees): consume buffered git helper

* feat(backup): redact pushed schedules by default

Unattended recurring pushes retain credential-bearing tables durably in
remote Git history, so backup enable --push now defaults to
--exclude-secrets; --include-secrets is the explicit full-fidelity
override (still warned). Local non-push schedules keep full fidelity for
complete restores.

* fix(backup): redact audit HMAC and OAuth pending state; tolerate absent backup_runs

Adds audit_identity_keys (audit HMAC key) and mcp_oauth_pending_authorizations
(live OAuth callback state) to the redaction inventory, and makes read-only
backup freshness treat a same-version database without the additive
backup_runs table as no recorded backups instead of failing before a
writable open converges the schema.

* fix(backup): restrict schedules to local gateways

* fix(snapshot): harden Git restore and redaction

* fix(backup): block pushes of adopted history

* fix(backup): contain commits and pairing secrets
2026-08-12 08:11:22 -07:00

94 lines
3.1 KiB
TypeScript

// Runtime gateway RPC helper shared by CLI commands that call the Gateway.
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
} from "../../packages/gateway-protocol/src/client-info.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { callGateway, isImplicitLocalGatewayTarget } from "../gateway/call.js";
import type { GatewayRpcOpts } from "./gateway-rpc.types.js";
import { parseTimeoutMsWithFallback } from "./parse-timeout.js";
import { withProgress } from "./progress.js";
type CallGatewayFromCliRuntimeExtra = {
clientName?: Parameters<typeof callGateway>[0]["clientName"];
mode?: Parameters<typeof callGateway>[0]["mode"];
deviceIdentity?: Parameters<typeof callGateway>[0]["deviceIdentity"];
signal?: Parameters<typeof callGateway>[0]["signal"];
expectFinal?: boolean;
progress?: boolean;
scopes?: Parameters<typeof callGateway>[0]["scopes"];
defaultTimeoutMs?: number;
timeoutMs?: number | null;
label?: string;
useStoredDeviceAuth?: boolean;
requiredStoredDeviceAuthScopes?: Parameters<
typeof callGateway
>[0]["requiredStoredDeviceAuthScopes"];
requireLocalBackendSharedAuth?: boolean;
};
type GatewayCliTransportRpcOpts = Omit<GatewayRpcOpts, "timeout"> & {
config?: OpenClawConfig;
timeout?: string | null;
localPortOverride?: number;
};
const DEFAULT_GATEWAY_RPC_TIMEOUT_MS = 30_000;
export async function isImplicitLocalGatewayTargetFromCliRuntime(
opts: GatewayCliTransportRpcOpts,
): Promise<boolean> {
return await isImplicitLocalGatewayTarget({
config: opts.config,
url: opts.url,
localPortOverride: opts.localPortOverride,
});
}
export async function callGatewayFromCliRuntime(
method: string,
opts: GatewayCliTransportRpcOpts,
params?: unknown,
extra?: CallGatewayFromCliRuntimeExtra,
) {
// Progress is disabled for JSON output so stdout stays parseable.
const showProgress = extra?.progress ?? opts.json !== true;
const timeoutMs =
extra?.timeoutMs !== undefined
? extra.timeoutMs
: opts.timeout === null
? null
: parseTimeoutMsWithFallback(
opts.timeout,
extra?.defaultTimeoutMs ?? DEFAULT_GATEWAY_RPC_TIMEOUT_MS,
{ invalidType: "error" },
);
return await withProgress(
{
label: extra?.label ?? `Gateway ${method}`,
indeterminate: true,
enabled: showProgress,
},
async () =>
await callGateway({
config: opts.config,
url: opts.url,
token: opts.token,
password: opts.password,
method,
params,
deviceIdentity: extra?.deviceIdentity,
expectFinal: extra?.expectFinal ?? Boolean(opts.expectFinal),
scopes: extra?.scopes,
useStoredDeviceAuth: extra?.useStoredDeviceAuth,
requiredStoredDeviceAuthScopes: extra?.requiredStoredDeviceAuthScopes,
requireLocalBackendSharedAuth: extra?.requireLocalBackendSharedAuth,
signal: extra?.signal,
timeoutMs,
localPortOverride: opts.localPortOverride,
clientName: extra?.clientName ?? GATEWAY_CLIENT_NAMES.CLI,
mode: extra?.mode ?? GATEWAY_CLIENT_MODES.CLI,
}),
);
}