Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ Versioning and the Keep a Changelog structure.

### Added

- Checkpoint create, list, restore, and delete operations scoped to
a running sandbox through `Sandbox.checkpoints()`.
- Reusable disk and full snapshot create, get, list, and delete operations,
plus sandbox restoration through `CreateSandboxRequest.fromSnapshot()`.
- Sandbox labels on create and response models, AND-matched label filtering in
`listSandboxes()`, and label merging through `Sandbox.setLabels()`.

### Changed

- Set the next Maven version and default user agent to `0.1.3`.

## 0.1.2 - 2026-09-21

### Fixed
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ capture requires a running source sandbox with no attached external disks.
Each account may retain up to five snapshots across both types. See the compile-checked
[snapshot example](examples/src/main/java/sh/createos/examples/snapshots/Snapshots.java).

## Checkpoint a directory

Checkpoints keep local file history inside one running sandbox. Stop
writers before create or restore; processes are not paused automatically.

```java
Checkpoint checkpoint = sandbox.checkpoints().create(
new CreateCheckpointRequest(
"/workspace/project", "before-update"));

List<Checkpoint> checkpoints = sandbox.checkpoints().list();
CheckpointRestoreResponse restored =
sandbox.checkpoints().restore(checkpoint.id());
sandbox.checkpoints().delete(checkpoint.id());
```

Restore always uses the checkpoint's original path. Checkpoints remain on the
sandbox disk and disappear with it. See the
[runnable example](examples/src/main/java/sh/createos/examples/checkpoints/README.md).

## Delegate access to one sandbox

An owner can create one delegated token for a sandbox. Plaintext is returned
Expand Down Expand Up @@ -401,6 +421,7 @@ validated by `ExamplesDocumentationTest`:
- [Command streaming](examples/src/main/java/sh/createos/examples/commandstreaming/README.md)
- [Files and snapshots](examples/src/main/java/sh/createos/examples/filesandsnapshots/README.md)
- [Reusable snapshots](examples/src/main/java/sh/createos/examples/snapshots/README.md)
- [Checkpoints](examples/src/main/java/sh/createos/examples/checkpoints/README.md)
- [Ingress preview](examples/src/main/java/sh/createos/examples/ingresspreview/README.md)
- [Private overlay network](examples/src/main/java/sh/createos/examples/network/README.md)
- [Custom template](examples/src/main/java/sh/createos/examples/customtemplate/README.md)
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Replace the final class with one of these:
| `managed-process` | `sh.createos.examples.managedprocess.ManagedProcessExample` |
| `network` | `sh.createos.examples.network.NetworkExample` |
| `snapshots` | `sh.createos.examples.snapshots.Snapshots` |
| `checkpoints` | `sh.createos.examples.checkpoints.Checkpoints` |

Examples create billable resources. Their `finally` blocks make a best effort
to destroy sandboxes and delete temporary account resources.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package sh.createos.examples.checkpoints;

import sh.createos.CreateOsClient;
import sh.createos.Sandbox;
import sh.createos.model.Checkpoint;
import sh.createos.model.CreateCheckpointRequest;
import sh.createos.model.CreateSandboxRequest;

/** Captures and restores local directory history inside one sandbox. */
public final class Checkpoints {
private static final String PATH = "/workspace/checkpoint-demo";

private Checkpoints() {}

/** Runs the example. */
public static void main(String[] arguments) {
CreateOsClient client =
CreateOsClient.builder().apiKey(System.getenv("CREATEOS_API_KEY")).build();
Sandbox sandbox =
client.createSandbox(
CreateSandboxRequest.builder("s-1vcpu-1gb").rootFileSystem("devbox:1").build());
Checkpoint checkpoint = null;
try {
sandbox.shell("mkdir -p " + PATH + " && printf 'version one' > " + PATH + "/value");
checkpoint = sandbox.checkpoints().create(new CreateCheckpointRequest(PATH, "before-update"));
sandbox.shell("printf 'version two' > " + PATH + "/value");
sandbox.checkpoints().restore(checkpoint.id());
String contents = sandbox.shell("cat " + PATH + "/value").result().standardOutput();
System.out.printf("restored %s: %s%n", checkpoint.id(), contents);
} finally {
try {
if (checkpoint != null) {
sandbox.checkpoints().delete(checkpoint.id());
}
} finally {
sandbox.destroy();
}
}
}
}
15 changes: 15 additions & 0 deletions examples/src/main/java/sh/createos/examples/checkpoints/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Checkpoints

Capture a directory, modify it, restore its earlier contents, and remove the
checkpoint.

```sh
export CREATEOS_API_KEY="..."
mvn verify
mvn dependency:build-classpath -Dmdep.outputFile=/tmp/createos-java-classpath
java -cp "target/classes:target/example-classes:$(cat /tmp/createos-java-classpath)" \
sh.createos.examples.checkpoints.Checkpoints
```

The example creates live resources and cleans them up when it finishes. Stop
all writers before creating or restoring a checkpoint.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>sh.createos</groupId>
<artifactId>createos-java-sdk</artifactId>
<version>0.1.2</version>
<version>0.1.3</version>
<name>CreateOS Java SDK</name>
<description>Java SDK for CreateOS Sandbox</description>
<url>https://github.com/NodeOps-app/createos-java-sdk</url>
Expand Down
74 changes: 74 additions & 0 deletions src/main/java/sh/createos/CheckpointsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package sh.createos;

import java.util.List;
import java.util.Map;
import sh.createos.internal.HttpTransport;
import sh.createos.model.Checkpoint;
import sh.createos.model.CheckpointRestoreResponse;
import sh.createos.model.CreateCheckpointRequest;

/** Local checkpoint operations for one running sandbox. */
public final class CheckpointsService {
private final Sandbox sandbox;

CheckpointsService(Sandbox sandbox) {
this.sandbox = sandbox;
}

/** Captures an eligible absolute directory. */
public Checkpoint create(CreateCheckpointRequest request) {
return sandbox
.transport()
.send(
"POST",
path("", ""),
Map.of(),
request,
RequestOptions.DEFAULT,
false,
Checkpoint.class);
}

/** Returns checkpoints oldest first. */
public List<Checkpoint> list() {
return sandbox
.transport()
.sendList("GET", path("", ""), Map.of(), RequestOptions.DEFAULT, false, Checkpoint.class);
}

/** Restores a checkpoint at its original directory path. */
public CheckpointRestoreResponse restore(String checkpointId) {
return sandbox
.transport()
.send(
"POST",
path(checkpointId, "/restore"),
Map.of(),
null,
RequestOptions.DEFAULT,
false,
CheckpointRestoreResponse.class);
}

/** Deletes a checkpoint record while retaining referenced layers. */
public void delete(String checkpointId) {
sandbox
.transport()
.send(
"DELETE",
path(checkpointId, ""),
Map.of(),
null,
RequestOptions.DEFAULT,
false,
Void.class);
}

private String path(String checkpointId, String suffix) {
String path = sandbox.path("/checkpoints");
if (!checkpointId.isEmpty()) {
path += "/" + HttpTransport.encodePathSegment(checkpointId);
}
return path + suffix;
}
}
2 changes: 1 addition & 1 deletion src/main/java/sh/createos/CreateOsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public static final class Builder {
private URI baseUri;
private HttpClient httpClient =
HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build();
private String userAgent = "createos-java-sdk/0.1.2";
private String userAgent = "createos-java-sdk/0.1.3";
private Duration timeout = Duration.ofSeconds(60);
private RetryPolicy retryPolicy =
new RetryPolicy(2, Duration.ofMillis(500), Duration.ofSeconds(30));
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/sh/createos/Sandbox.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public final class Sandbox {
private final FilesService files;
private final ProcessesService processes;
private final ComputerService computer;
private final CheckpointsService checkpoints;
private SandboxData data;

Sandbox(HttpTransport transport, SandboxData data) {
Expand All @@ -43,6 +44,7 @@ public final class Sandbox {
this.files = new FilesService(this);
this.processes = new ProcessesService(this);
this.computer = new ComputerService(this);
this.checkpoints = new CheckpointsService(this);
}

/** Returns the sandbox identifier. */
Expand Down Expand Up @@ -95,6 +97,11 @@ public ComputerService computer() {
return computer;
}

/** Returns local checkpoint operations for this sandbox. */
public CheckpointsService checkpoints() {
return checkpoints;
}

/** Returns a separate sandbox handle using a delegated token for runtime operations. */
public Sandbox withAccessToken(String token) {
if (token == null || token.isBlank()) {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/sh/createos/model/Checkpoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package sh.createos.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;

/** Local file history retained inside one sandbox. */
@JsonIgnoreProperties(ignoreUnknown = true)
public record Checkpoint(
String id,
String path,
String name,
@JsonProperty("parent_id") String parentId,
@JsonProperty("created_at") Instant createdAt) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package sh.createos.model;

import com.fasterxml.jackson.annotation.JsonProperty;

/** Identifies the restored checkpoint and its original directory path. */
public record CheckpointRestoreResponse(
@JsonProperty("checkpoint_id") String checkpointId, String path) {}
13 changes: 13 additions & 0 deletions src/main/java/sh/createos/model/CreateCheckpointRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package sh.createos.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Objects;

/** Selects a directory whose local file history should be captured. */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record CreateCheckpointRequest(String path, String name) {
/** Validates the required directory path. */
public CreateCheckpointRequest {
Objects.requireNonNull(path, "path");
}
}
67 changes: 67 additions & 0 deletions src/test/java/sh/createos/CreateOsClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sh.createos.model.CreateCheckpointRequest;
import sh.createos.model.CreateSandboxRequest;
import sh.createos.model.HealthResponse;
import sh.createos.model.Shape;
Expand Down Expand Up @@ -196,6 +197,72 @@ void patchesLabelsAndUpdatesCachedState() {
assertEquals(Map.of("environment", "test", "team", "platform"), sandbox.labels());
}

@Test
void managesCheckpointLifecycleAndEncodesIds() {
AtomicInteger step = new AtomicInteger();
server.createContext(
"/v1/sandboxes/sb-1",
exchange -> {
int index = step.getAndIncrement();
String path = exchange.getRequestURI().getRawPath();
if (index == 0) {
respond(
exchange,
200,
"{\"status\":\"success\",\"data\":{\"id\":\"sb-1\",\"status\":\"running\"}}");
} else if (index == 1) {
assertEquals("POST", exchange.getRequestMethod());
assertEquals("/v1/sandboxes/sb-1/checkpoints", path);
String body =
new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
assertTrue(body.contains("\"path\":\"/workspace/app\""));
assertTrue(body.contains("\"name\":\"before-update\""));
respond(
exchange,
200,
"{\"status\":\"success\",\"data\":{\"id\":\"cp_first\","
+ "\"path\":\"/workspace/app\","
+ "\"created_at\":\"2026-11-05T12:00:00Z\"}}");
} else if (index == 2) {
assertEquals("GET", exchange.getRequestMethod());
respond(
exchange,
200,
"{\"status\":\"success\",\"data\":[{\"id\":\"cp_first\","
+ "\"path\":\"/workspace/app\","
+ "\"created_at\":\"2026-11-05T12:00:00Z\"}]}");
} else if (index == 3) {
assertEquals("POST", exchange.getRequestMethod());
assertEquals("/v1/sandboxes/sb-1/checkpoints/cp_first%2Fsegment/restore", path);
assertEquals(0, exchange.getRequestBody().readAllBytes().length);
respond(
exchange,
200,
"{\"status\":\"success\",\"data\":{"
+ "\"checkpoint_id\":\"cp_first/segment\","
+ "\"path\":\"/workspace/app\"}}");
} else {
assertEquals("DELETE", exchange.getRequestMethod());
assertEquals("/v1/sandboxes/sb-1/checkpoints/cp_first%2Fsegment", path);
respond(exchange, 200, "{\"status\":\"success\",\"data\":{}}");
}
});

Sandbox sandbox = client().getSandbox("sb-1");
var checkpoint =
sandbox
.checkpoints()
.create(new CreateCheckpointRequest("/workspace/app", "before-update"));
var checkpoints = sandbox.checkpoints().list();
var restored = sandbox.checkpoints().restore("cp_first/segment");
assertEquals("/workspace/app", restored.path());
sandbox.checkpoints().delete("cp_first/segment");

assertEquals("cp_first", checkpoint.id());
assertEquals(1, checkpoints.size());
assertEquals(5, step.get());
}

@Test
void exposesApiErrorMetadata() {
server.createContext(
Expand Down
Loading