Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
f8a9a58
refactor(onboard): lower cognitive complexity ratchet to 245
cv Jun 14, 2026
65d7578
refactor(rebuild): lower cognitive complexity ratchet to 244
cv Jun 14, 2026
433ba8a
refactor(rebuild): lower cognitive complexity ratchet to 243
cv Jun 14, 2026
30e2b10
refactor(onboard): move setupNim validation helpers
cv Jun 14, 2026
bd07191
Merge branch 'ratchet/245-setup-nim-cognitive-complexity' into ratche…
cv Jun 14, 2026
fda1c05
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 14, 2026
cdc58d7
test(rebuild): cover interactive confirmation
cv Jun 14, 2026
2879093
refactor(rebuild): lower cognitive complexity ratchet to 225
cv Jun 15, 2026
df63d1a
refactor(rebuild): lower cognitive complexity ratchet to 224
cv Jun 15, 2026
494c641
refactor(rebuild): lower cognitive complexity ratchet to 215
cv Jun 15, 2026
2f250de
refactor(rebuild): lower cognitive complexity ratchet to 186
cv Jun 15, 2026
843e3a4
refactor(snapshot): lower cognitive complexity ratchet to 185
cv Jun 15, 2026
8b83cbf
chore(lint): lower cognitive complexity ratchet to 184 (#5431)
cv Jun 15, 2026
557f501
refactor(onboard): keep snapshot ratchet step net-neutral
cv Jun 15, 2026
7778b05
Merge branch 'main' into ratchet/245-setup-nim-cognitive-complexity
cv Jun 15, 2026
b451a5b
fix(onboard): preserve local NIM fallback state
cv Jun 15, 2026
9e3ddf5
Merge branch 'ratchet/245-setup-nim-cognitive-complexity' into ratche…
cv Jun 15, 2026
08743b0
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
17913db
Merge branch 'ratchet/243-rebuild-confirm-cognitive-complexity' into …
cv Jun 15, 2026
18a4325
Merge branch 'ratchet/225-rebuild-single-agent-guard' into ratchet/22…
cv Jun 15, 2026
cf9cc11
Merge branch 'ratchet/224-rebuild-registry-guard' into ratchet/215-re…
cv Jun 15, 2026
f26ffe1
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
af4f61a
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
74968a3
refactor(rebuild): isolate messaging config hydration
cv Jun 15, 2026
ed3170a
test(onboard): cover setupNim selection validator state
cv Jun 15, 2026
4ef8d6f
Merge branch 'ratchet/245-setup-nim-cognitive-complexity' into ratche…
cv Jun 15, 2026
3df8744
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
8d3daaa
Merge branch 'ratchet/243-rebuild-confirm-cognitive-complexity' into …
cv Jun 15, 2026
c8f5794
Merge branch 'ratchet/225-rebuild-single-agent-guard' into ratchet/22…
cv Jun 15, 2026
4499f83
Merge branch 'ratchet/224-rebuild-registry-guard' into ratchet/215-re…
cv Jun 15, 2026
463e1d7
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
fd21dbe
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
9194819
Merge remote-tracking branch 'origin/main' into ratchet/244-rebuild-c…
cv Jun 15, 2026
00724d3
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
57dafc8
Merge branch 'ratchet/243-rebuild-confirm-cognitive-complexity' into …
cv Jun 15, 2026
374e4a8
Merge branch 'ratchet/225-rebuild-single-agent-guard' into ratchet/22…
cv Jun 15, 2026
6758a6d
Merge branch 'ratchet/224-rebuild-registry-guard' into ratchet/215-re…
cv Jun 15, 2026
e6b39bb
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
fa2c734
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
ebe72ba
test(onboard): align sandbox registration fixture
cv Jun 15, 2026
a23904d
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
af81f07
Merge branch 'ratchet/243-rebuild-confirm-cognitive-complexity' into …
cv Jun 15, 2026
d9ad1a0
Merge branch 'ratchet/225-rebuild-single-agent-guard' into ratchet/22…
cv Jun 15, 2026
b2ab482
Merge branch 'ratchet/224-rebuild-registry-guard' into ratchet/215-re…
cv Jun 15, 2026
56b891e
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
e51f221
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
cb69607
test(rebuild): cover active session warning
cv Jun 15, 2026
86b83a0
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
7b61d67
Merge branch 'ratchet/243-rebuild-confirm-cognitive-complexity' into …
cv Jun 15, 2026
e512f8f
Merge branch 'ratchet/225-rebuild-single-agent-guard' into ratchet/22…
cv Jun 15, 2026
3926914
Merge branch 'ratchet/224-rebuild-registry-guard' into ratchet/215-re…
cv Jun 15, 2026
bcddede
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
b9d6824
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
94402c3
Merge remote-tracking branch 'origin/main' into ratchet/243-rebuild-c…
cv Jun 15, 2026
98902a7
Merge branch 'ratchet/243-rebuild-confirm-cognitive-complexity' into …
cv Jun 15, 2026
e28e8e7
Merge branch 'ratchet/225-rebuild-single-agent-guard' into ratchet/22…
cv Jun 15, 2026
ac58322
Merge branch 'ratchet/224-rebuild-registry-guard' into ratchet/215-re…
cv Jun 15, 2026
fccda94
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
a57b88a
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
04da8e5
Merge remote-tracking branch 'origin/main' into ratchet/225-rebuild-s…
cv Jun 15, 2026
d066d12
Merge branch 'ratchet/225-rebuild-single-agent-guard' into ratchet/22…
cv Jun 15, 2026
3fe883f
Merge branch 'ratchet/224-rebuild-registry-guard' into ratchet/215-re…
cv Jun 15, 2026
17320b6
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
a753a58
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
d6d5bfe
Merge remote-tracking branch 'origin/main' into ratchet/224-rebuild-r…
cv Jun 15, 2026
a679c21
Merge branch 'ratchet/224-rebuild-registry-guard' into ratchet/215-re…
cv Jun 15, 2026
cf2e341
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
3033a95
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
51492b1
Merge remote-tracking branch 'origin/main' into ratchet/215-rebuild-v…
cv Jun 15, 2026
af54450
Merge branch 'ratchet/215-rebuild-version-header' into ratchet/186-re…
cv Jun 15, 2026
8198b5e
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
8ec4bd6
fix(rebuild): target legacy local credential bypass
cv Jun 15, 2026
b69ad81
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
dfe58bf
test(rebuild): cover messaging staging preflight
cv Jun 15, 2026
74a5e0e
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
6d17d33
Merge remote-tracking branch 'origin/main' into ratchet/186-rebuild-c…
cv Jun 15, 2026
c2a115a
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
8eca2e2
chore(rebuild): apply formatter
cv Jun 15, 2026
a7b3277
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
1d8239e
fix(rebuild): fail closed on missing remote session credential
cv Jun 15, 2026
bc918e9
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
804dc16
fix(rebuild): use registry provider for credential preflight
cv Jun 15, 2026
7043fd0
Merge branch 'ratchet/186-rebuild-credential-preflight' into ratchet/…
cv Jun 15, 2026
5c9776f
Merge remote-tracking branch 'origin/main' into ratchet/185-snapshot-…
cv Jun 15, 2026
af3257b
test(snapshot): cover create failure branches
cv Jun 15, 2026
0e2b7e7
Merge remote-tracking branch 'origin/main' into ratchet/185-snapshot-…
cv Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"noExcessiveCognitiveComplexity": {
"level": "error",
"options": {
"maxAllowedComplexity": 186
"maxAllowedComplexity": 184
}
}
},
Expand Down
22 changes: 11 additions & 11 deletions src/lib/actions/sandbox/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,16 @@ function preflightRebuildCredentials(
return false;
}

function hydrateMessagingConfigForRebuild(sandboxName: string, log: (msg: string) => void): void {
const rebuildSession = onboardSession.loadSession();
const hydratedMessagingConfig = hydrateMessagingChannelConfig(
getStoredMessagingChannelConfig(sandboxName, rebuildSession),
);
if (hydratedMessagingConfig) {
log(`Stashed messaging config for rebuild: ${Object.keys(hydratedMessagingConfig).join(",")}`);
}
}

function printRebuildVersionSummary(
sandboxName: string,
agentName: string,
Expand Down Expand Up @@ -595,17 +605,7 @@ export async function rebuildSandbox(
// destructive. The manifest plan in registry is the durable source; legacy
// session channel fields are read only as compatibility fallback by
// getStoredMessagingChannelConfig().
{
const rebuildSession = onboardSession.loadSession();
const hydratedMessagingConfig = hydrateMessagingChannelConfig(
getStoredMessagingChannelConfig(sandboxName, rebuildSession),
);
if (hydratedMessagingConfig) {
log(
`Stashed messaging config for rebuild: ${Object.keys(hydratedMessagingConfig).join(",")}`,
);
}
}
hydrateMessagingConfigForRebuild(sandboxName, log);

// Version check — show what's changing
const versionCheck = sandboxVersion.checkAgentVersion(sandboxName);
Expand Down
80 changes: 73 additions & 7 deletions src/lib/actions/sandbox/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const shieldsMock = vi.hoisted(() => {
const isShieldsDownMock = vi.fn(() => true);
let isShieldsDownExport: unknown = isShieldsDownMock;
return {
isShieldsDownMock,
getIsShieldsDownExport: () => isShieldsDownExport,
setIsShieldsDownExport: (value: unknown) => {
isShieldsDownExport = value;
},
};
});

const backupSandboxStateMock = vi.fn();
const captureOpenshellMock = vi.fn(() => ({ status: 0, output: "alpha Ready\n" }));
const dockerInspectMock = vi.fn(() => ({ status: 0, stdout: "true\n" }));
const getSandboxMock = vi.fn(() => null);
const findBackupMock = vi.fn();
const getSandboxMock = vi.fn(() => null);
const isGatewayHealthyMock = vi.fn(() => true);
const isShieldsDownMock = vi.fn();
const listBackupsMock = vi.fn<() => Array<Record<string, unknown>>>(() => []);
const parseLiveSandboxNamesMock = vi.fn(() => new Set(["alpha"]));
const restoreSandboxStateMock = vi.fn();
Expand Down Expand Up @@ -47,7 +58,9 @@ vi.mock("../../runtime-recovery", () => ({
}));

vi.mock("../../shields", () => ({
isShieldsDown: isShieldsDownMock,
get isShieldsDown() {
return shieldsMock.getIsShieldsDownExport();
},
repairMutableConfigPerms: vi.fn(() => ({ applied: true, verified: true, errors: [] })),
}));

Expand Down Expand Up @@ -77,12 +90,13 @@ vi.mock("./destroy", () => ({
describe("runSandboxSnapshot", () => {
beforeEach(() => {
vi.clearAllMocks();
shieldsMock.setIsShieldsDownExport(shieldsMock.isShieldsDownMock);
shieldsMock.isShieldsDownMock.mockReturnValue(true);
captureOpenshellMock.mockReturnValue({ status: 0, output: "alpha Ready\n" });
dockerInspectMock.mockReturnValue({ status: 0, stdout: "true\n" });
findBackupMock.mockReturnValue({ match: null });
getSandboxMock.mockReturnValue(null);
isGatewayHealthyMock.mockReturnValue(true);
isShieldsDownMock.mockReturnValue(true);
listBackupsMock.mockReturnValue([]);
restoreSandboxStateMock.mockReturnValue({
success: true,
Expand All @@ -99,9 +113,8 @@ describe("runSandboxSnapshot", () => {
});

it("refuses snapshot creation before backup when the shields gate helper is unavailable", async () => {
shieldsMock.setIsShieldsDownExport(undefined);
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
const shields = await import("../../shields");
vi.mocked(shields).isShieldsDown = undefined as never;
const { runSandboxSnapshot } = await import("./snapshot");

await expect(runSandboxSnapshot("alpha", { kind: "create" })).rejects.toMatchObject({
Expand All @@ -112,7 +125,6 @@ describe("runSandboxSnapshot", () => {
expect(consoleError.mock.calls.flat().join("\n")).toContain(
"Cannot verify shields state. Refusing to create snapshot.",
);
vi.mocked(shields).isShieldsDown = isShieldsDownMock as never;
});

it("creates a named snapshot after gateway, liveness, and shields checks pass", async () => {
Expand Down Expand Up @@ -172,4 +184,58 @@ describe("runSandboxSnapshot", () => {
expect(output).toContain("/tmp/alpha/v2");
expect(output).toContain("2 snapshot(s). Restore with:");
});

it("refuses snapshot creation before backup when the sandbox is not live", async () => {
parseLiveSandboxNamesMock.mockReturnValue(new Set(["beta"]));
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
const { runSandboxSnapshot } = await import("./snapshot");

await expect(runSandboxSnapshot("alpha", { kind: "create" })).rejects.toMatchObject({
exitCode: 1,
});

expect(backupSandboxStateMock).not.toHaveBeenCalled();
expect(consoleError.mock.calls.flat().join("\n")).toContain(
"Sandbox 'alpha' is not running. Cannot create snapshot.",
);
});

it("prints backup error details when snapshot creation fails with an error", async () => {
backupSandboxStateMock.mockReturnValue({
success: false,
error: "tar exploded",
failedDirs: [],
failedFiles: [],
});
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "log").mockImplementation(() => {});
const { runSandboxSnapshot } = await import("./snapshot");

await expect(runSandboxSnapshot("alpha", { kind: "create" })).rejects.toMatchObject({
exitCode: 1,
});

expect(backupSandboxStateMock).toHaveBeenCalledWith("alpha", { name: null });
expect(consoleError.mock.calls.flat().join("\n")).toContain("tar exploded");
});

it("prints failed dirs and files when snapshot creation fails without an error", async () => {
backupSandboxStateMock.mockReturnValue({
success: false,
failedDirs: ["workspace", "skills"],
failedFiles: ["openclaw.json"],
});
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "log").mockImplementation(() => {});
const { runSandboxSnapshot } = await import("./snapshot");

await expect(runSandboxSnapshot("alpha", { kind: "create" })).rejects.toMatchObject({
exitCode: 1,
});

const errors = consoleError.mock.calls.flat().join("\n");
expect(errors).toContain("Snapshot failed.");
expect(errors).toContain("Failed directories: workspace, skills");
expect(errors).toContain("Failed files: openclaw.json");
});
});
98 changes: 50 additions & 48 deletions src/lib/actions/sandbox/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,11 @@ function verifyRestoreDestinationOnOwnGateway(targetSandbox: string): void {
}

function isSnapshotCreationAllowedByShields(sandboxName: string): boolean {
// Snapshot creation is a shields/policy boundary. If a packaged or mocked
// CommonJS interop surface ever omits the helper, fail closed before any
// backup side effect instead of throwing an ambiguous TypeError.
// Snapshot creation is a shields/policy boundary. Production builds should
// always export this helper, but stale compiled artifacts, package-boundary
// skew, or test doubles can present a missing CommonJS interop surface. There
// is no safe runtime source fix once snapshot creation has started, so keep
// this as permanent defense-in-depth and fail closed before backup side effects.
const isShieldsDown = shields.isShieldsDown;
if (typeof isShieldsDown !== "function") {
console.error(" Cannot verify shields state. Refusing to create snapshot.");
Expand All @@ -339,57 +341,57 @@ function isSnapshotCreationAllowedByShields(sandboxName: string): boolean {
return isShieldsDown(sandboxName);
}

function runSnapshotCreate(
sandboxName: string,
request: Extract<SnapshotRequest, { kind: "create" }>,
): void {
const liveNames = requireLiveSandboxesOnSandboxGateway(
sandboxName,
" Failed to query live sandbox state from OpenShell.",
);
if (!liveNames.has(sandboxName)) {
console.error(` Sandbox '${sandboxName}' is not running. Cannot create snapshot.`);
snapshotExit(1);
}
if (!isSnapshotCreationAllowedByShields(sandboxName)) {
console.error(" Cannot create snapshot while shields are up.");
console.error(` Run \`${CLI_NAME} ${sandboxName} shields down\` first, then retry.`);
snapshotExit(1);
}
const label = request.name ? ` (--name ${request.name})` : "";
console.log(` Creating snapshot of '${sandboxName}'${label}...`);
const result = sandboxState.backupSandboxState(sandboxName, { name: request.name ?? null });
if (result.success) {
const manifest = result.manifest!;
const entry = sandboxState.findBackup(sandboxName, manifest.timestamp).match ?? manifest;
const v = formatSnapshotVersion(entry);
const nameSuffix = entry.name ? ` name=${entry.name}` : "";
const itemSummary = `${result.backedUpDirs.length} directories, ${result.backedUpFiles.length} files`;
console.log(` ${G}✓${R} Snapshot ${v}${nameSuffix} created (${itemSummary})`);
console.log(` ${manifest.backupPath}`);
return;
}
if (result.error) {
console.error(` ${result.error}`);
} else {
console.error(" Snapshot failed.");
if (result.failedDirs.length > 0) {
console.error(` Failed directories: ${result.failedDirs.join(", ")}`);
}
if (result.failedFiles.length > 0) {
console.error(` Failed files: ${result.failedFiles.join(", ")}`);
}
}
snapshotExit(1);
}

export async function runSandboxSnapshot(
sandboxName: string,
request: SnapshotRequest = { kind: "help" },
) {
switch (request.kind) {
case "create": {
// Select the sandbox's gateway before the health probe and `sandbox list`
// — Docker/VM-driver health and the sandbox listing both require the
// sandbox's gateway to be the active one, otherwise a sandbox registered
// on a non-default `NEMOCLAW_GATEWAY_PORT` fails health-check before the
// subsequent list ever runs.
const liveNames = requireLiveSandboxesOnSandboxGateway(
sandboxName,
" Failed to query live sandbox state from OpenShell.",
);
if (!liveNames.has(sandboxName)) {
console.error(` Sandbox '${sandboxName}' is not running. Cannot create snapshot.`);
snapshotExit(1);
}
if (!isSnapshotCreationAllowedByShields(sandboxName)) {
console.error(" Cannot create snapshot while shields are up.");
console.error(` Run \`${CLI_NAME} ${sandboxName} shields down\` first, then retry.`);
snapshotExit(1);
}
const label = request.name ? ` (--name ${request.name})` : "";
console.log(` Creating snapshot of '${sandboxName}'${label}...`);
const result = sandboxState.backupSandboxState(sandboxName, { name: request.name ?? null });
if (result.success) {
// Virtual snapshotVersion is only assigned by listBackups, so re-resolve
// the just-created snapshot by its timestamp to get a valid v<N>.
const manifest = result.manifest!;
const entry = sandboxState.findBackup(sandboxName, manifest.timestamp).match ?? manifest;
const v = formatSnapshotVersion(entry);
const nameSuffix = entry.name ? ` name=${entry.name}` : "";
const itemSummary = `${result.backedUpDirs.length} directories, ${result.backedUpFiles.length} files`;
console.log(` ${G}\u2713${R} Snapshot ${v}${nameSuffix} created (${itemSummary})`);
console.log(` ${manifest.backupPath}`);
} else {
if (result.error) {
console.error(` ${result.error}`);
} else {
console.error(" Snapshot failed.");
if (result.failedDirs.length > 0) {
console.error(` Failed directories: ${result.failedDirs.join(", ")}`);
}
if (result.failedFiles.length > 0) {
console.error(` Failed files: ${result.failedFiles.join(", ")}`);
}
}
snapshotExit(1);
}
runSnapshotCreate(sandboxName, request);
break;
}
case "list": {
Expand Down
6 changes: 2 additions & 4 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {
applyCloudFallbackSelection,
clearNimContainerBeforeRetry,
createRemoteModelValidator,
requireProviderChoice,
}: typeof import("./onboard/setup-nim-selection") = require("./onboard/setup-nim-selection");
const inferenceInputCapability = require("./onboard/inference-input-capability");
const { cleanupTempDir }: typeof import("./onboard/temp-files") = require("./onboard/temp-files");
Expand Down Expand Up @@ -4071,10 +4072,7 @@ async function setupNim(
});
}

if (!selected) {
console.error(" No provider was selected.");
process.exit(1);
}
selected = requireProviderChoice(selected);
if (selected.key !== "hermesProvider") {
hermesAuthMethod = null;
hermesToolGateways = [];
Expand Down
8 changes: 8 additions & 0 deletions src/lib/onboard/setup-nim-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ type ProviderChoice = {
key: string;
};

export function requireProviderChoice<T extends ProviderChoice>(selected: T | undefined): T {
if (!selected) {
console.error(" No provider was selected.");
process.exit(1);
}
return selected;
}

type RemoteProviderConfig = {
label: string;
endpointUrl: string;
Expand Down