mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
Adds named storage locations as a generic, pluggable capability, with backup as its first consumer. - Core storage owner (src/storage): storage.locations config, a location marker that binds identity (runtime never creates it, so unplugged disks and different disks at the same path are refused), client-side streaming encryption (scrypt key from a SecretRef passphrase, per-object HKDF keys, AES-256-GCM segments), and a built-in filesystem provider for external disks and mounts. - Plugin SDK: api.registerStorageProvider plus manifest contracts.storageProviders; providers move opaque bytes only. - Bundled cloudflare plugin: an r2 provider over the S3 API with conditional writes and bounded multipart uploads; auto-enabled when a location uses provider "r2". - Backups: backup create --to <location> with verified archives, UTC retention, list/verify/restore --from, Gateway-owned offsite schedules (installed Git schedules unchanged), per-installation namespace claims fenced at publication and deletion, backup record for external jobs, backup.status RPC, Doctor/status hints, and a Systems page Backups section. No config or state migration; the storage section is new and optional. Proof: live R2 and mounted-disk round trips, namespace takeover trace, and a published 2026.9.7 upgrade cell with an existing Git backup schedule.
223 lines
7.7 KiB
JavaScript
223 lines
7.7 KiB
JavaScript
import {
|
|
classifyReleaseTrain,
|
|
compareReleaseVersions,
|
|
parseReleaseVersion,
|
|
} from "./release-version.mjs";
|
|
import catalog from "./upgrade-survivor-scenarios.json" with { type: "json" };
|
|
|
|
const UPGRADE_SURVIVOR_SCENARIOS = Object.freeze(catalog.scenarios);
|
|
// Frozen Codex allowlist recipes retain their assertion-only scenario.
|
|
export const UPGRADE_SURVIVOR_ASSERTION_SCENARIOS = Object.freeze([
|
|
...UPGRADE_SURVIVOR_SCENARIOS,
|
|
...catalog.assertionOnlyScenarios,
|
|
]);
|
|
|
|
// Oldest release line supported by the operator-state upgrade regression gate.
|
|
export const OLDEST_SUPPORTED_UPGRADE_SURVIVOR_BASELINE = "2026.6.34";
|
|
export const MINIMUM_UPGRADE_SURVIVOR_BASELINE = "2026.6.1";
|
|
export const CUSTOM_PLUGIN_SIBLINGS_BASELINE = "openclaw@2026.9.4";
|
|
|
|
// 2026.9.7 retired code mode; older baselines must still seed the migration specimen.
|
|
export function usesStructuredToolSearchAtBaseline(baselineVersion) {
|
|
const comparison = compareReleaseVersions(baselineVersion ?? "", "2026.9.7");
|
|
return comparison !== null && comparison >= 0;
|
|
}
|
|
|
|
const scenarioMinimumBaselines = new Map([
|
|
["custom-plugin-siblings", CUSTOM_PLUGIN_SIBLINGS_BASELINE],
|
|
["legacy-operator-state", `openclaw@${OLDEST_SUPPORTED_UPGRADE_SURVIVOR_BASELINE}`],
|
|
["mobile-pairing-reconnect", "openclaw@2026.7.1"],
|
|
["watchos-direct-node", "openclaw@2026.8.1"],
|
|
]);
|
|
|
|
// These black-box scenarios are implemented entirely by the current trusted
|
|
// release harness and treat the selected tree only as the package under test.
|
|
const TRUSTED_HARNESS_OWNED_SCENARIOS = new Set([
|
|
"mobile-pairing-reconnect",
|
|
"abandoned-update",
|
|
"projects-doctor",
|
|
"channel-owner-policy",
|
|
"projects-startup-migration",
|
|
"workshop-doctor-recovery",
|
|
"update-report-recovery",
|
|
"dreaming-cron-doctor",
|
|
]);
|
|
|
|
export function isTrustedHarnessOwnedUpgradeSurvivorScenario(scenario) {
|
|
return TRUSTED_HARNESS_OWNED_SCENARIOS.has(scenario);
|
|
}
|
|
|
|
// Registry proof needs its artifact contract; versioned auth fixtures exercise
|
|
// legacy import rather than native state from every baseline in a broad sweep.
|
|
// Platform pairing probes run only through explicit or dedicated scheduled
|
|
// qualification until their runtime cost justifies aggregate release coverage.
|
|
const aggregateScenarios = UPGRADE_SURVIVOR_SCENARIOS.filter(
|
|
(scenario) =>
|
|
scenario !== "abandoned-update" &&
|
|
scenario !== "backup-schedule" &&
|
|
scenario !== "missing-configured-plugin-migration" &&
|
|
scenario !== "missing-load-path" &&
|
|
scenario !== "projects-doctor" &&
|
|
scenario !== "channel-owner-policy" &&
|
|
scenario !== "projects-startup-migration" &&
|
|
scenario !== "workshop-doctor-recovery" &&
|
|
scenario !== "update-report-recovery" &&
|
|
scenario !== "dreaming-cron-doctor" &&
|
|
scenario !== "mobile-pairing-reconnect" &&
|
|
scenario !== "watchos-direct-node" &&
|
|
scenario !== "prerelease-plugin-registry" &&
|
|
scenario !== "auth-profile-v2026-7-2-beta-5" &&
|
|
scenario !== "recovery-cleanup",
|
|
);
|
|
const scenarioAliases = new Map([
|
|
["reported-issues", aggregateScenarios.filter((scenario) => scenario !== "sqlite-volume")],
|
|
["far-reaching", aggregateScenarios],
|
|
]);
|
|
|
|
export function normalizeUpgradeSurvivorBaselineSpec(raw) {
|
|
const value = raw?.trim() ?? "";
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
const spec = value.startsWith("openclaw@") ? value : `openclaw@${value}`;
|
|
if (
|
|
!/^openclaw@(?:alpha|beta|latest|[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[0-9]+|alpha\.[0-9]+|beta\.[0-9]+))?)$/u.test(
|
|
spec,
|
|
)
|
|
) {
|
|
throw new Error(
|
|
`invalid published upgrade survivor baseline: ${JSON.stringify(
|
|
value,
|
|
)}. Expected openclaw@latest, openclaw@beta, openclaw@alpha, or openclaw@YYYY.M.PATCH.`,
|
|
);
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
export function parseUpgradeSurvivorBaselineSpecs(raw) {
|
|
if (!raw) {
|
|
return [];
|
|
}
|
|
return [
|
|
...new Set(
|
|
raw
|
|
.split(/[,\s]+/u)
|
|
.map(normalizeUpgradeSurvivorBaselineSpec)
|
|
.filter((spec) => spec !== undefined),
|
|
),
|
|
];
|
|
}
|
|
|
|
// Historical receipts retain syntax-only parsing; active harnesses enforce the floor.
|
|
export function assertSupportedUpgradeSurvivorBaselineSpec(spec) {
|
|
if (!spec || /^openclaw@(alpha|beta|latest)$/u.test(spec)) {
|
|
return;
|
|
}
|
|
const version = parseReleaseVersion(spec.replace(/^openclaw@/u, ""));
|
|
if (!version) {
|
|
throw new Error(`invalid published upgrade survivor baseline: ${spec}`);
|
|
}
|
|
if (compareReleaseVersions(version.baseVersion, MINIMUM_UPGRADE_SURVIVOR_BASELINE) === -1) {
|
|
throw new Error(
|
|
`Published upgrade survivor baselines must be ${MINIMUM_UPGRADE_SURVIVOR_BASELINE} or newer; got ${spec}. Upgrade pre-June installs through OpenClaw 2026.9.5 and run Doctor first.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function normalizeUpgradeSurvivorScenario(raw) {
|
|
const value = raw?.trim() ?? "";
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
if (!UPGRADE_SURVIVOR_SCENARIOS.includes(value)) {
|
|
throw new Error(
|
|
`invalid published upgrade survivor scenario: ${JSON.stringify(
|
|
value,
|
|
)}. Expected one of: ${UPGRADE_SURVIVOR_SCENARIOS.join(", ")}, reported-issues, or far-reaching.`,
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function parseUpgradeSurvivorScenarios(raw) {
|
|
if (!raw) {
|
|
return [];
|
|
}
|
|
return [
|
|
...new Set(
|
|
raw
|
|
.split(/[,\s]+/u)
|
|
.map((token) => token.trim())
|
|
.filter(Boolean)
|
|
.flatMap((token) => scenarioAliases.get(token) ?? [token])
|
|
.map(normalizeUpgradeSurvivorScenario)
|
|
.filter((scenario) => scenario !== undefined),
|
|
),
|
|
];
|
|
}
|
|
|
|
function parsePublishedReleaseVersion(spec) {
|
|
const match = /^openclaw@([0-9]{4})\.([0-9]+)\.([0-9]+)/u.exec(spec ?? "");
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
return {
|
|
year: Number(match[1]),
|
|
month: Number(match[2]),
|
|
patch: Number(match[3]),
|
|
};
|
|
}
|
|
|
|
function comparePublishedReleaseVersion(a, b) {
|
|
return a.year - b.year || a.month - b.month || a.patch - b.patch;
|
|
}
|
|
|
|
export function supportsUpgradeSurvivorScenarioAtBaseline(scenario, baselineSpec) {
|
|
if (scenario === "backup-schedule") {
|
|
return baselineSpec === "openclaw@2026.9.7";
|
|
}
|
|
if (scenario === "missing-load-path") {
|
|
const release = parseReleaseVersion((baselineSpec ?? "").replace(/^openclaw@/u, ""));
|
|
// Floating tags are checked again against the installed baseline before seeding.
|
|
if (!release) {
|
|
return true;
|
|
}
|
|
// #113324 first shipped in beta.5; July's frozen line retained the older CLI guard.
|
|
const train = classifyReleaseTrain(release);
|
|
const frozenJuly =
|
|
release.year === 2026 &&
|
|
release.month === 7 &&
|
|
(train === "extended-stable" || train === "unsupported-extended-stable-correction");
|
|
const comparison = compareReleaseVersions(release.version, "2026.7.2-beta.5");
|
|
return !frozenJuly && comparison !== null && comparison >= 0;
|
|
}
|
|
const version = parsePublishedReleaseVersion(baselineSpec);
|
|
if (scenario === "dreaming-cron-doctor") {
|
|
return baselineSpec === "openclaw@2026.9.6";
|
|
}
|
|
if (
|
|
scenario === "projects-doctor" ||
|
|
scenario === "channel-owner-policy" ||
|
|
scenario === "projects-startup-migration"
|
|
) {
|
|
return baselineSpec === "openclaw@2026.9.4";
|
|
}
|
|
if (scenario === "abandoned-update") {
|
|
return baselineSpec === "openclaw@2026.9.4" || baselineSpec === "openclaw@2026.9.3";
|
|
}
|
|
if (scenario === "missing-configured-plugin-migration") {
|
|
return baselineSpec === "openclaw@2026.9.2";
|
|
}
|
|
if (scenario === "workshop-doctor-recovery") {
|
|
return baselineSpec === "openclaw@2026.9.4";
|
|
}
|
|
if (scenario === "update-report-recovery") {
|
|
return baselineSpec === "openclaw@2026.9.6";
|
|
}
|
|
const minimumBaseline = scenarioMinimumBaselines.get(scenario);
|
|
return (
|
|
!minimumBaseline ||
|
|
!version ||
|
|
comparePublishedReleaseVersion(version, parsePublishedReleaseVersion(minimumBaseline)) >= 0
|
|
);
|
|
}
|