Elixir SDK for Pocketenv
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add sandbox backup API and types

Add create/list/restore backup functions to Pocketenv and Sandbox; add
Sandbox.Types.Backup struct and from_map/1. Update README, CHANGELOG and
tests; bump version to 0.1.9

+338 -3
+8
CHANGELOG.md
··· 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 7 8 + ## [0.1.9] - 2026-04-07 9 + 10 + ### Added 11 + - Backup management: `Pocketenv.create_backup/3`, `list_backups/2`, `restore_backup/2` and pipe-friendly `Sandbox.create_backup/3`, `Sandbox.list_backups/2`, `Sandbox.restore_backup/3` 12 + - `Sandbox.Types.Backup` struct with `id`, `directory`, `description`, `expires_at`, and `created_at` fields 13 + - Optional `:description` and `:ttl` options for `create_backup` 14 + 8 15 ## [0.1.8] - 2026-04-05 9 16 10 17 ### Added ··· 63 70 - MIT License 64 71 - Package description in `mix.exs` 65 72 73 + [0.1.9]: https://github.com/pocketenv-io/pocketenv-elixir/compare/v0.1.8...v0.1.9 66 74 [0.1.8]: https://github.com/pocketenv-io/pocketenv-elixir/compare/v0.1.7...v0.1.8 67 75 [0.1.7]: https://github.com/pocketenv-io/pocketenv-elixir/compare/v0.1.6...v0.1.7 68 76 [0.1.6]: https://github.com/pocketenv-io/pocketenv-elixir/compare/v0.1.5...v0.1.6
+42
README.md
··· 235 235 overridden via the `:storage_url` app config key or the 236 236 `POCKETENV_STORAGE_URL` environment variable. 237 237 238 + #### Backups 239 + 240 + ```elixir 241 + # Create a backup of a directory 242 + {:ok, _} = sandbox |> Sandbox.create_backup("/workspace") 243 + {:ok, _} = sandbox |> Sandbox.create_backup("/workspace", description: "before migration", ttl: 86400) 244 + 245 + # List all backups 246 + {:ok, backups} = sandbox |> Sandbox.list_backups() 247 + 248 + # Restore a backup by id 249 + {:ok, _} = sandbox |> Sandbox.restore_backup("backup-id") 250 + ``` 251 + 252 + | Function | Returns | Description | 253 + |---|---|---| 254 + | `Sandbox.create_backup(sandbox, directory, opts)` | `{:ok, map()}` | Create a backup of a directory inside the sandbox | 255 + | `Sandbox.list_backups(sandbox, opts)` | `{:ok, [%Backup{}]}` | List all backups for the sandbox | 256 + | `Sandbox.restore_backup(sandbox, backup_id, opts)` | `{:ok, map()}` | Restore a backup by id | 257 + 258 + ##### `create_backup/3` options 259 + 260 + | Option | Type | Default | Description | 261 + |---|---|---|---| 262 + | `:description` | `string` | `nil` | Human-readable label for the backup | 263 + | `:ttl` | `integer` | `nil` | Time-to-live in seconds before the backup expires | 264 + | `:token` | `string` | global config | Bearer token override | 265 + 238 266 --- 239 267 240 268 ## Types ··· 305 333 avatar: String.t() | nil, 306 334 created_at: String.t() | nil, 307 335 updated_at: String.t() | nil 336 + } 337 + ``` 338 + 339 + ### `%Sandbox.Types.Backup{}` 340 + 341 + Returned in the list by `Sandbox.list_backups/2` and `Pocketenv.list_backups/2`. 342 + 343 + ``` 344 + %Sandbox.Types.Backup{ 345 + id: String.t(), 346 + directory: String.t(), 347 + description: String.t() | nil, 348 + expires_at: String.t() | nil, 349 + created_at: String.t() 308 350 } 309 351 ``` 310 352
+43
lib/pocketenv.ex
··· 259 259 @spec put_tailscale_auth_key(String.t(), String.t(), keyword()) :: 260 260 {:ok, map()} | {:error, term()} 261 261 defdelegate put_tailscale_auth_key(sandbox_id, auth_key, opts \\ []), to: API 262 + 263 + # --------------------------------------------------------------------------- 264 + # Backups 265 + # --------------------------------------------------------------------------- 266 + 267 + @doc """ 268 + Creates a backup of a directory inside a sandbox. 269 + 270 + ## Options 271 + 272 + - `:description` — a human-readable label for the backup. 273 + - `:ttl` — time-to-live in seconds before the backup expires. 274 + - `:token` — bearer token override. 275 + 276 + ## Example 277 + 278 + {:ok, _} = Pocketenv.create_backup(sandbox.id, "/workspace") 279 + {:ok, _} = Pocketenv.create_backup(sandbox.id, "/workspace", description: "before migration") 280 + """ 281 + @spec create_backup(String.t(), String.t(), keyword()) :: 282 + {:ok, map()} | {:error, term()} 283 + defdelegate create_backup(sandbox_id, directory, opts \\ []), to: API 284 + 285 + @doc """ 286 + Lists all backups for a sandbox. 287 + 288 + ## Example 289 + 290 + {:ok, backups} = Pocketenv.list_backups(sandbox.id) 291 + """ 292 + @spec list_backups(String.t(), keyword()) :: 293 + {:ok, [Sandbox.Types.Backup.t()]} | {:error, term()} 294 + defdelegate list_backups(sandbox_id, opts \\ []), to: API 295 + 296 + @doc """ 297 + Restores a backup by its id. 298 + 299 + ## Example 300 + 301 + {:ok, _} = Pocketenv.restore_backup("backup-id") 302 + """ 303 + @spec restore_backup(String.t(), keyword()) :: {:ok, map()} | {:error, term()} 304 + defdelegate restore_backup(backup_id, opts \\ []), to: API 262 305 end
+36 -1
lib/pocketenv/api.ex
··· 5 5 6 6 alias Pocketenv.Client 7 7 alias Pocketenv.Crypto 8 - alias Sandbox.Types.{ExecResult, Port, Profile, Secret, SshKey, TailscaleAuthKey} 8 + alias Sandbox.Types.{Backup, ExecResult, Port, Profile, Secret, SshKey, TailscaleAuthKey} 9 9 10 10 @default_base "at://did:plc:aturpi2ls3yvsmhc6wybomun/io.pocketenv.sandbox/openclaw" 11 11 ··· 303 303 Client.post( 304 304 "/xrpc/io.pocketenv.sandbox.putTailscaleAuthKey", 305 305 %{"id" => sandbox_id, "authKey" => encrypted, "redacted" => redacted}, 306 + take_token(opts) 307 + ) 308 + end 309 + 310 + # --------------------------------------------------------------------------- 311 + # Backups 312 + # --------------------------------------------------------------------------- 313 + 314 + def create_backup(sandbox_id, directory, opts \\ []) do 315 + body = 316 + %{"directory" => directory} 317 + |> maybe_put("description", Keyword.get(opts, :description)) 318 + |> maybe_put("ttl", Keyword.get(opts, :ttl)) 319 + 320 + Client.post( 321 + "/xrpc/io.pocketenv.sandbox.createBackup", 322 + body, 323 + take_token(opts) ++ [params: %{"id" => sandbox_id}] 324 + ) 325 + end 326 + 327 + def list_backups(sandbox_id, opts \\ []) do 328 + case Client.get( 329 + "/xrpc/io.pocketenv.sandbox.getBackups", 330 + take_token(opts) ++ [params: %{"id" => sandbox_id}] 331 + ) do 332 + {:ok, %{"backups" => items}} -> {:ok, Enum.map(items, &Backup.from_map/1)} 333 + {:error, _} = err -> err 334 + end 335 + end 336 + 337 + def restore_backup(backup_id, opts \\ []) do 338 + Client.post( 339 + "/xrpc/io.pocketenv.sandbox.restoreBackup", 340 + %{"backupId" => backup_id}, 306 341 take_token(opts) 307 342 ) 308 343 end
+80 -1
lib/sandbox.ex
··· 31 31 """ 32 32 33 33 alias Pocketenv.API 34 - alias Sandbox.Types.{ExecResult, Port, Profile, Secret, SshKey, TailscaleAuthKey} 34 + alias Sandbox.Types.{Backup, ExecResult, Port, Profile, Secret, SshKey, TailscaleAuthKey} 35 35 36 36 @type status :: :running | :stopped | :unknown 37 37 ··· 510 510 511 511 def set_tailscale_auth_key(%__MODULE__{} = sandbox, auth_key, opts) do 512 512 API.put_tailscale_auth_key(sandbox.id, auth_key, opts) 513 + end 514 + 515 + # --------------------------------------------------------------------------- 516 + # Backups 517 + # --------------------------------------------------------------------------- 518 + 519 + @doc """ 520 + Creates a backup of a directory inside the sandbox. 521 + 522 + ## Parameters 523 + 524 + - `directory` — the sandbox path to back up (e.g. `"/workspace"`). 525 + 526 + ## Options 527 + 528 + - `:description` — a human-readable label for the backup. 529 + - `:ttl` — time-to-live in seconds before the backup expires. 530 + - `:token` — bearer token override. 531 + 532 + ## Example 533 + 534 + sandbox |> Sandbox.create_backup("/workspace") 535 + sandbox |> Sandbox.create_backup("/workspace", description: "before migration", ttl: 86400) 536 + """ 537 + @spec create_backup(t() | {:ok, t()}, String.t(), keyword()) :: 538 + {:ok, map()} | {:error, term()} 539 + def create_backup(sandbox_or_result, directory, opts \\ []) 540 + 541 + def create_backup({:ok, %__MODULE__{} = sandbox}, directory, opts), 542 + do: create_backup(sandbox, directory, opts) 543 + 544 + def create_backup(%__MODULE__{} = sandbox, directory, opts) do 545 + API.create_backup(sandbox.id, directory, opts) 546 + end 547 + 548 + @doc """ 549 + Lists all backups for the sandbox. 550 + 551 + ## Options 552 + 553 + - `:token` — bearer token override. 554 + 555 + ## Returns 556 + 557 + `{:ok, [%Sandbox.Types.Backup{}]}` 558 + 559 + ## Example 560 + 561 + {:ok, backups} = sandbox |> Sandbox.list_backups() 562 + """ 563 + @spec list_backups(t() | {:ok, t()}, keyword()) :: 564 + {:ok, [Backup.t()]} | {:error, term()} 565 + def list_backups(sandbox_or_result, opts \\ []) 566 + def list_backups({:ok, %__MODULE__{} = sandbox}, opts), do: list_backups(sandbox, opts) 567 + 568 + def list_backups(%__MODULE__{} = sandbox, opts) do 569 + API.list_backups(sandbox.id, opts) 570 + end 571 + 572 + @doc """ 573 + Restores a backup by its id. 574 + 575 + ## Options 576 + 577 + - `:token` — bearer token override. 578 + 579 + ## Example 580 + 581 + sandbox |> Sandbox.restore_backup("backup-id") 582 + """ 583 + @spec restore_backup(t() | {:ok, t()}, String.t(), keyword()) :: 584 + {:ok, map()} | {:error, term()} 585 + def restore_backup(sandbox_or_result, backup_id, opts \\ []) 586 + 587 + def restore_backup({:ok, %__MODULE__{} = sandbox}, backup_id, opts), 588 + do: restore_backup(sandbox, backup_id, opts) 589 + 590 + def restore_backup(%__MODULE__{}, backup_id, opts) do 591 + API.restore_backup(backup_id, opts) 513 592 end 514 593 515 594 # ---------------------------------------------------------------------------
+25
lib/sandbox/types.ex
··· 138 138 } 139 139 end 140 140 end 141 + 142 + defmodule Backup do 143 + @moduledoc "Represents a sandbox backup." 144 + 145 + @type t :: %__MODULE__{ 146 + id: String.t(), 147 + directory: String.t(), 148 + description: String.t() | nil, 149 + expires_at: String.t() | nil, 150 + created_at: String.t() 151 + } 152 + 153 + defstruct [:id, :directory, :description, :expires_at, :created_at] 154 + 155 + @doc "Build a Backup from the raw API map." 156 + def from_map(map) when is_map(map) do 157 + %__MODULE__{ 158 + id: map["id"], 159 + directory: map["directory"], 160 + description: map["description"], 161 + expires_at: map["expiresAt"], 162 + created_at: map["createdAt"] 163 + } 164 + end 165 + end 141 166 end
+1 -1
mix.exs
··· 4 4 def project do 5 5 [ 6 6 app: :pocketenv_ex, 7 - version: "0.1.8", 7 + version: "0.1.9", 8 8 elixir: "~> 1.15", 9 9 start_permanent: Mix.env() == :prod, 10 10 deps: deps(),
+103
test/pocketenv_test.exs
··· 679 679 end 680 680 681 681 # --------------------------------------------------------------------------- 682 + # Sandbox.Types.Backup 683 + # --------------------------------------------------------------------------- 684 + 685 + describe "Sandbox.Types.Backup.from_map/1" do 686 + test "parses a backup with all fields" do 687 + raw = %{ 688 + "id" => "bkp-001", 689 + "directory" => "/workspace", 690 + "description" => "before migration", 691 + "expiresAt" => "2024-12-31T00:00:00Z", 692 + "createdAt" => "2024-06-01T00:00:00Z" 693 + } 694 + 695 + backup = Sandbox.Types.Backup.from_map(raw) 696 + 697 + assert backup.id == "bkp-001" 698 + assert backup.directory == "/workspace" 699 + assert backup.description == "before migration" 700 + assert backup.expires_at == "2024-12-31T00:00:00Z" 701 + assert backup.created_at == "2024-06-01T00:00:00Z" 702 + end 703 + 704 + test "handles missing optional fields gracefully" do 705 + raw = %{ 706 + "id" => "bkp-002", 707 + "directory" => "/home/user", 708 + "createdAt" => "2024-01-01T00:00:00Z" 709 + } 710 + 711 + backup = Sandbox.Types.Backup.from_map(raw) 712 + 713 + assert backup.id == "bkp-002" 714 + assert backup.directory == "/home/user" 715 + assert backup.description == nil 716 + assert backup.expires_at == nil 717 + assert backup.created_at == "2024-01-01T00:00:00Z" 718 + end 719 + end 720 + 721 + # --------------------------------------------------------------------------- 722 + # Sandbox backup pipe methods — API surface 723 + # --------------------------------------------------------------------------- 724 + 725 + describe "Sandbox backup methods – arities" do 726 + setup do 727 + fns = Sandbox.__info__(:functions) 728 + {:ok, fns: fns} 729 + end 730 + 731 + test "create_backup/3 is defined", %{fns: fns} do 732 + assert {:create_backup, 2} in fns 733 + assert {:create_backup, 3} in fns 734 + end 735 + 736 + test "list_backups/2 is defined", %{fns: fns} do 737 + assert {:list_backups, 1} in fns 738 + assert {:list_backups, 2} in fns 739 + end 740 + 741 + test "restore_backup/3 is defined", %{fns: fns} do 742 + assert {:restore_backup, 2} in fns 743 + assert {:restore_backup, 3} in fns 744 + end 745 + end 746 + 747 + describe "Sandbox backup methods – {:ok, struct} passthrough" do 748 + test "create_backup/3 accepts {:ok, %Sandbox{}} and does not raise FunctionClauseError" do 749 + sandbox = %Sandbox{id: "sbx-1", name: "test", status: :running, installs: 0} 750 + assert match?({:error, _}, Sandbox.create_backup({:ok, sandbox}, "/workspace")) 751 + end 752 + 753 + test "list_backups/2 accepts {:ok, %Sandbox{}} and does not raise FunctionClauseError" do 754 + sandbox = %Sandbox{id: "sbx-1", name: "test", status: :running, installs: 0} 755 + assert match?({:error, _}, Sandbox.list_backups({:ok, sandbox})) 756 + end 757 + 758 + test "restore_backup/3 accepts {:ok, %Sandbox{}} and does not raise FunctionClauseError" do 759 + sandbox = %Sandbox{id: "sbx-1", name: "test", status: :running, installs: 0} 760 + assert match?({:error, _}, Sandbox.restore_backup({:ok, sandbox}, "bkp-001")) 761 + end 762 + end 763 + 764 + describe "Sandbox backup methods – error propagation" do 765 + test "create_backup/3 raises FunctionClauseError on {:error, reason}" do 766 + assert_raise FunctionClauseError, fn -> 767 + Sandbox.create_backup({:error, :not_found}, "/workspace") 768 + end 769 + end 770 + 771 + test "list_backups/2 raises FunctionClauseError on {:error, reason}" do 772 + assert_raise FunctionClauseError, fn -> 773 + Sandbox.list_backups({:error, :not_found}) 774 + end 775 + end 776 + 777 + test "restore_backup/3 raises FunctionClauseError on {:error, reason}" do 778 + assert_raise FunctionClauseError, fn -> 779 + Sandbox.restore_backup({:error, :not_found}, "bkp-001") 780 + end 781 + end 782 + end 783 + 784 + # --------------------------------------------------------------------------- 682 785 # Pocketenv public API surface 683 786 # --------------------------------------------------------------------------- 684 787