akernel-sdk 0.2.0rc2__tar.gz → 0.2.0rc4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/PKG-INFO +515 -28
  2. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/README.md +513 -26
  3. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/__init__.py +28 -0
  4. akernel_sdk-0.2.0rc4/akernel_sdk/_backends/adx.py +1535 -0
  5. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_backends/base.py +114 -4
  6. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_backends/errors.py +64 -0
  7. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_backends/registry.py +10 -2
  8. akernel_sdk-0.2.0rc4/akernel_sdk/_client.py +104 -0
  9. akernel_sdk-0.2.0rc4/akernel_sdk/_dualmethod.py +42 -0
  10. akernel_sdk-0.2.0rc4/akernel_sdk/_fork.py +91 -0
  11. akernel_sdk-0.2.0rc4/akernel_sdk/_sandboxes.py +86 -0
  12. akernel_sdk-0.2.0rc4/akernel_sdk/_snapshots.py +74 -0
  13. akernel_sdk-0.2.0rc4/akernel_sdk/commands.py +173 -0
  14. akernel_sdk-0.2.0rc4/akernel_sdk/sandbox.py +1350 -0
  15. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/types.py +65 -6
  16. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk.egg-info/PKG-INFO +515 -28
  17. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk.egg-info/SOURCES.txt +5 -0
  18. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk.egg-info/requires.txt +1 -1
  19. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/pyproject.toml +2 -2
  20. akernel_sdk-0.2.0rc2/akernel_sdk/_backends/adx.py +0 -691
  21. akernel_sdk-0.2.0rc2/akernel_sdk/commands.py +0 -126
  22. akernel_sdk-0.2.0rc2/akernel_sdk/sandbox.py +0 -664
  23. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_addresses.py +0 -0
  24. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_backends/__init__.py +0 -0
  25. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_dockercontext.py +0 -0
  26. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_dockerfile.py +0 -0
  27. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_dockerfile_launch.py +0 -0
  28. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_dockerfile_runner.py +0 -0
  29. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_instance.py +0 -0
  30. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_pty_transport.py +0 -0
  31. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_resource_api.py +0 -0
  32. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_resources.py +0 -0
  33. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_sandbox_resources.py +0 -0
  34. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/_tls.py +0 -0
  35. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/cli.py +0 -0
  36. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/filesystem.py +0 -0
  37. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/pty.py +0 -0
  38. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk/py.typed +0 -0
  39. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk.egg-info/dependency_links.txt +0 -0
  40. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk.egg-info/entry_points.txt +0 -0
  41. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/akernel_sdk.egg-info/top_level.txt +0 -0
  42. {akernel_sdk-0.2.0rc2 → akernel_sdk-0.2.0rc4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akernel-sdk
3
- Version: 0.2.0rc2
3
+ Version: 0.2.0rc4
4
4
  Summary: Python SDK and CLI for AKernel remote sandboxes
5
5
  Author: AKernel Authors
6
6
  License-Expression: Apache-2.0
@@ -23,7 +23,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Classifier: Topic :: System :: Distributed Computing
24
24
  Requires-Python: >=3.10
25
25
  Description-Content-Type: text/markdown
26
- Requires-Dist: adx-environment==0.1.0rc2
26
+ Requires-Dist: adx-environment==0.1.0rc3
27
27
  Requires-Dist: idna<4,>=3
28
28
  Requires-Dist: websockets>=10.0
29
29
  Requires-Dist: dockerfile-parse>=2.0.1
@@ -53,6 +53,8 @@ PTYs, port forwarding, and reverse tunnels.
53
53
  - [Filesystem](#filesystem)
54
54
  - [Interactive PTYs](#interactive-ptys)
55
55
  - [Port forwarding](#port-forwarding)
56
+ - [Lifecycle compatibility](#lifecycle-compatibility)
57
+ - [Reusable snapshots](#reusable-snapshots)
56
58
  - [Local failover and reload](#local-failover-and-reload)
57
59
  - [Reverse tunnels](#reverse-tunnels)
58
60
  - [Rootfs and mounts](#rootfs-and-mounts)
@@ -413,10 +415,11 @@ prerequisites are available.
413
415
  Kata and Firecracker require at least one cluster node whose sandboxd instance
414
416
  successfully initialized the requested runtime with a usable `/dev/kvm`
415
417
  device. Nodes without KVM remain available for runsc workloads and do not
416
- advertise either VM runtime. The bundled Firecracker configuration enables
417
- read-only virtio-fs for OCI/Nydus image roots and read-only host directories,
418
- while retaining EROFS image roots and mounts. OCI image mounts, writable host
419
- binds, GPUs, and nested KVM remain unsupported.
418
+ advertise either VM runtime. The bundled Firecracker configuration disables
419
+ virtio-fs and uses EROFS image roots and mounts. OCI/Nydus image roots and
420
+ HostPath directories require an operator-enabled compatible virtio-fs profile;
421
+ see [enabling Firecracker directory access](#enabling-firecracker-directory-access).
422
+ OCI image mounts, GPUs, and nested KVM remain unsupported.
420
423
 
421
424
  The all-in-one image can optionally package a native Linux `runc` backend.
422
425
  Operators build it with `AKERNEL_ENABLE_RUNC=true` and enable it explicitly
@@ -482,6 +485,28 @@ handle.close_stdin()
482
485
  result = handle.wait(timeout=15)
483
486
  ```
484
487
 
488
+ Commands can be reattached by stable `command_id` (preferred) or an observed
489
+ PID without submitting them again. A returned handle binds its stable ID once;
490
+ reusing the same PID for another command cannot redirect its identity, wait,
491
+ stdin or kill operations. Direct PID operations remain convenience lookups:
492
+
493
+ ```python
494
+ handle = sandbox.commands.run("sleep 2; printf done", background=True)
495
+ command_id = handle.command_id
496
+ sandbox.close()
497
+ attached = Sandbox.connect(sandbox.sandbox_id)
498
+ try:
499
+ result = attached.commands.connect(command_id).wait(timeout=30, check=True)
500
+ finally:
501
+ attached.close()
502
+ ```
503
+
504
+ `check=True` on run/wait opts into `CommandExitException` for nonzero exit,
505
+ retaining stdout, stderr and exit_code, and `TimeoutException` for an unfinished
506
+ observation. The default continues returning `CommandResult`. Authentication,
507
+ permission, missing-resource, timeout, rate-limit and service-busy exceptions
508
+ subclass `BackendOperationError` and retain structured service context.
509
+
485
510
  Foreground commands return a backend-neutral `CommandResult`. Background
486
511
  commands return an AKernel `CommandHandle`; its lifecycle operations are
487
512
  delegated to the selected backend.
@@ -568,6 +593,363 @@ with Sandbox(port_forwardings=[8080]) as sandbox:
568
593
  deployment operator explicitly wants the direct ADX Ingress address instead of the
569
594
  public gateway.
570
595
 
596
+ ## Lifecycle compatibility
597
+
598
+ These source APIs follow E2B's Python naming while preserving AKernel's existing
599
+ constructor, ID, detached cleanup, command-result and creation-budget contracts.
600
+ They are not present in an older published wheel merely because its package
601
+ version matches this checkout. Pin a tested source revision when installing.
602
+
603
+ ```python
604
+ from akernel_sdk import Client, SandboxQuery, SandboxState
605
+
606
+ client = Client(api_url="https://akernel.example.com", api_key="<tenant-key>",
607
+ verify_tls=True)
608
+ # An explicit Client captures its own API/gateway/token. It neither changes
609
+ # os.environ nor redirects top-level Sandbox operations. Specify gateway_address
610
+ # when the data gateway differs. Addresses must be HTTP(S) origins without paths.
611
+ source = client.Sandbox.create(image="python:3.13-slim", idle_timeout=600)
612
+ identity = source.sandbox_id # source.id remains supported
613
+ try:
614
+ source.files.write("/tmp/progress", "saved")
615
+ assert source.pause() # beta_pause() is an alias; already paused returns False
616
+ source.close() # closes local transports, retaining the remote sandbox
617
+ restored = client.Sandbox.connect(identity)
618
+ try:
619
+ assert restored.sandbox_id == identity
620
+ assert restored.files.read("/tmp/progress") == "saved"
621
+ finally:
622
+ restored.close()
623
+ print(client.Sandbox.get_info(identity)) # get_by_id(identity) also supported
624
+ pages = client.Sandbox.list(limit=20,
625
+ query=SandboxQuery(state=[SandboxState.RUNNING, SandboxState.PAUSED]))
626
+ while pages.has_next:
627
+ for info in pages.next_items():
628
+ print(info.sandbox_id, info.state)
629
+ finally:
630
+ source.close()
631
+ client.Sandbox.kill(identity) # bool: False means already absent
632
+ ```
633
+
634
+ `connect` attaches to Running or resumes Paused under the **same logical ID**.
635
+ It does not create a replacement, rerun a Dockerfile, restart a command, or create
636
+ `cwd`. It waits for Pausing/Resuming to settle and errors on other states. Its
637
+ `timeout` is a logical connection/resume wait budget; transport retry margins and
638
+ tunnel initialization can add elapsed time. Supply `port_forwardings` and
639
+ `reverse_tunnel` again to reconstruct local routing; do not reuse old PTYs or
640
+ streams. A failed attachment closes only local resources and never deletes the
641
+ existing sandbox. As with fresh sandboxes, a connected handle's context manager
642
+ owns remote deletion unless `detached=True`; use `close()` to retain it.
643
+
644
+ Pause creates an anonymous in-place checkpoint with permanent retention until
645
+ explicit deletion. It is separate from `create_snapshot()`'s reusable catalog
646
+ snapshot and `from_snapshot()`'s new ID. The SDK stops its own reverse tunnel
647
+ before pause so that its reconnect loop cannot immediately demand-resume the
648
+ sandbox. Other connected clients or inbound traffic may still resume it.
649
+ Pause errors and unknown outcomes propagate; False means authoritatively paused.
650
+
651
+ | Operation | Ownership and result |
652
+ | --- | --- |
653
+ | `sandbox.close()` | Idempotent local disconnect; does not delete the remote sandbox. Closing PTYs terminates their interactive processes. |
654
+ | `sandbox.kill()` / context exit | Existing behavior: owned handles delete remote state and close locally; detached handles only close locally. Returns None. |
655
+ | `sandbox.kill(delete_remote=True)` | Explicit remote deletion, including detached or previously closed handles. Failures remain retryable. |
656
+ | `Sandbox.kill(id)` / `Sandbox.kill_by_id(id)` | Canonical by-ID deletion, returns bool. False means already absent; permission/network/unknown errors propagate. |
657
+ | `Sandbox.delete(sandbox_id=id)` | Explicit by-ID deletion returning None; existing positional `Sandbox.delete(name)` continues to delete a named detached sandbox. |
658
+ | `sandbox.get_info()` / `Sandbox.get_info(id)` | Handle or canonical directory query; `get_by_id(id)` is an explicit class alias. |
659
+ | `Sandbox.list(query=..., limit=20, next_token=None)` | Lazy paginator. `next_items()` fetches one filtered page, `has_next` and `next_token` retain progress. Errors do not advance the cursor. Default states are Running and Paused. |
660
+
661
+ ### Timeouts and supported backend surface
662
+
663
+ `create_timeout` on the constructor and factory/restore `timeout` are logical
664
+ creation budgets, not maximum lifetimes. `schedule_timeout` bounds scheduling;
665
+ `idle_timeout` measures inactivity. These meanings remain unchanged.
666
+
667
+ This SDK change uses the released `adx-environment==0.1.0rc3` and existing
668
+ cluster operations. It does not require a new backend or bundle rollout.
669
+ Custom user metadata and filtering, a running-lifetime deadline/set_timeout,
670
+ live command-output callbacks, and per-command user selection are outside this
671
+ API change. They require separately reviewed backend capabilities; the SDK does
672
+ not emulate them with scheduler labels, idle timers or output replay.
673
+
674
+ ## Reusable snapshots
675
+
676
+ These APIs require `akernel-sdk>=0.2.0rc4`; `0.2.0rc3` and earlier wheels do
677
+ not contain them. Install a compatible release candidate with `--pre` or install
678
+ this revision from source. The cluster/runtime must support reusable
679
+ memory/filesystem snapshots. Unsupported operations fail with the backend's
680
+ structured error.
681
+
682
+ ```python
683
+ from akernel_sdk import Sandbox
684
+
685
+ snapshot = None
686
+ try:
687
+ with Sandbox(image="python:3.12-slim") as source:
688
+ source.files.write("/tmp/progress", "turn 10")
689
+ snapshot = source.create_snapshot(name="turn-10", timeout=300)
690
+ print(Sandbox.get_snapshot(snapshot.snapshot_id))
691
+ # Source resumes after capture; it can also be deleted independently.
692
+
693
+ with Sandbox.create(snapshot.snapshot_id) as restored:
694
+ assert restored.files.read("/tmp/progress") == "turn 10"
695
+ finally:
696
+ if snapshot is not None:
697
+ Sandbox.delete_snapshot(snapshot.snapshot_id)
698
+ ```
699
+
700
+ | API | Result / semantics |
701
+ | --- | --- |
702
+ | `sandbox.create_snapshot(name=None, *, timeout=300)` | `SnapshotInfo(snapshot_id, names)`; captures runtime memory and filesystem, then the source continues. Timeout is 1..3600 seconds. |
703
+ | `Sandbox.get_snapshot(snapshot_id)` | Query catalog metadata by opaque ID; pending deletion may still return metadata. |
704
+ | `Sandbox.list_snapshots(name=None, limit=None, next_token=None)` | Lazy `SnapshotPaginator`; `limit` is the page size. Returns the accessible catalog, optionally filtered by name. |
705
+ | `Sandbox.delete_snapshot(snapshot_id)` | Idempotent when absent, returns `None` after acceptance; active references can reject deletion with `SNAPSHOT_IN_USE`. |
706
+ | `Sandbox.create(snapshot_id, ...)` | New `Sandbox`, new ID and fresh commands/files/PTY facades; does not delete or change the source. Accepts an ID or `SnapshotInfo`. |
707
+ | `Sandbox.from_snapshot(snapshot_id, ...)` | Retained compatibility entry point with the same options, validation, errors and ownership as `create`. |
708
+
709
+ The naming and paginator pattern follow E2B, including
710
+ `Sandbox.create(snapshot.snapshot_id)`. Without a snapshot ID,
711
+ `Sandbox.create(image=...)` creates a fresh sandbox, and `Sandbox.create()`
712
+ uses the cluster default rootfs. Existing constructors and
713
+ `Sandbox.from_snapshot(...)` remain supported. AKernel does not resolve E2B
714
+ or agentenv template aliases or another platform's snapshot IDs.
715
+ Listing is a class-level catalog operation, including when accessed through an
716
+ instance; delete returns `None` because the backend does not distinguish absent from
717
+ newly deleted snapshots. Only memory/filesystem capture is exposed; no
718
+ unsupported filesystem-only mode or fabricated timestamps/storage metrics.
719
+
720
+ ```python
721
+ pages = Sandbox.list_snapshots(name="turn-10", limit=20)
722
+ while pages.has_next:
723
+ for snapshot in pages.next_items():
724
+ print(snapshot.snapshot_id, snapshot.names)
725
+ # Save pages.next_token between pages to resume a listing later.
726
+ ```
727
+
728
+ Restore inherits captured runtime and CPU/memory geometry rather than applying
729
+ constructor defaults. It does not replay a Dockerfile, image ENTRYPOINT, CMD or
730
+ working-directory creation. `cwd` only sets the command default. `idle_timeout`
731
+ (default 300), `schedule_timeout` (30), `timeout` (schedule timeout + 60), `name`,
732
+ `detached`, `failover`, `node_id`, `port_forwardings`, `mounts`, `reverse_tunnel`
733
+ and `network_policy` configure the new environment/client. Rebind the local
734
+ `HttpReverseTunnel` target explicitly; source client sockets/PTYs/Python handles
735
+ are not restored. Supply `network_policy` explicitly to preserve restrictions:
736
+ the pinned ADX backend does not inherit captured execution policy on restore.
737
+ An omitted or empty policy uses the backend default. A supplied policy is
738
+ applied during creation, before the restored workload is made available.
739
+ For mounted external data, consistency and availability remain the caller's
740
+ responsibility; restore does not copy node-local HostPath storage.
741
+
742
+ Snapshots outlive their source and do not expire automatically. Delete them
743
+ explicitly. Current ADX rejects deletion with `BackendOperationError` code
744
+ `SNAPSHOT_IN_USE` while environments or templates retain references. Release
745
+ those users and retry; a rejected delete has not marked the snapshot for deletion.
746
+ Older deployments can accept deletion before references are released. On either
747
+ version, an acknowledged delete prevents new restores and never kills existing
748
+ sandboxes; physical collection is asynchronous. Catalog records may remain
749
+ queryable while Deleting, so a successful
750
+ `get_snapshot()` is not a readiness or physical-deletion check. Once collection
751
+ marks the snapshot Deleted, the backend returns not-found. The
752
+ current backend descriptor exposes only ID and names, not catalog state; the
753
+ service validates readiness on `create()` / `from_snapshot()`. Storage locality
754
+ and cross-node restore depend on cluster configuration. Capturing complete
755
+ process state does not imply a full physical copy: incremental/reflink storage
756
+ is a runtime/deployment concern, not selected
757
+ by this API. These APIs are separate from anonymous failover/reload checkpoints.
758
+
759
+ A remote snapshot does not save the host harness conversation or cursor, model
760
+ requests, or external service state. Quiesce tools and model streams and pair
761
+ local harness state with the returned ID. A restore can execute concurrently
762
+ with its source; the harness must fence the old instance before allowing a
763
+ recovered agent to proceed. It must not also relaunch an agent whose process
764
+ memory has been restored.
765
+
766
+ `BackendOperationError` preserves status, code, retry/outcome, request ID and
767
+ operation ID when supplied. Snapshot creation makes one request attempt. Restore
768
+ uses the native transport's same-operation retries (stable request ID); the
769
+ public SDK does not initiate a second logical restore. On unknown outcome,
770
+ reconcile using the supplied identity instead of blindly calling create/restore
771
+ again. Before creation is confirmed, native unknown-outcome context can contain
772
+ a lookup name rather than a canonical ID; do not pass it blindly to `ak delete`.
773
+ After creation is confirmed, failure context retains the canonical ID. Read-only
774
+ route checks retry temporary 409/503 responses within a 10-second retry window,
775
+ without recreating the sandbox. A blocking transport call is separately bounded.
776
+ Initialization failures, including interruption during tunnel startup, attempt
777
+ to delete the known-created clone and preserve the original error. Failed
778
+ rollback errors remain in `cleanup_errors` for diagnosis and explicit retry.
779
+ See [`examples/snapshots.py`](./examples/snapshots.py).
780
+
781
+ `timeout` must be at least `schedule_timeout + 60` seconds; shorter budgets raise
782
+ `ValueError` before a create request. This prevents the pinned native SDK from
783
+ silently increasing the supplied budget. It is the frontend's logical creation
784
+ budget, not a wall-clock deadline for the entire Python call: transport margins,
785
+ tunnel/readiness setup, facade binding and cleanup can add elapsed time.
786
+
787
+ ## Fork a running sandbox
788
+
789
+ `sandbox.fork(count=N)` captures one reusable memory/filesystem snapshot and
790
+ derives N new running sandboxes. It returns an ordered `list[Sandbox | Exception]`
791
+ with exactly one result per requested child. A child failure does not kill
792
+ successful siblings; a capture failure raises for the whole call. The source
793
+ keeps its ID and idle policy and continues after capture.
794
+
795
+ ```python
796
+ from akernel_sdk import ForkCleanupError, Sandbox
797
+
798
+ children = []
799
+ pending_snapshot = None
800
+ try:
801
+ with Sandbox(image="python:3.13-slim-bookworm") as source:
802
+ # Explicitly acknowledge this fresh example's unrestricted policy;
803
+ # older API versions omit creation ownership from their responses.
804
+ source.update_network_policy(None)
805
+ source.files.write("/tmp/state", "captured")
806
+ try:
807
+ results = source.fork(count=3)
808
+ except ForkCleanupError as error:
809
+ # Preserve handles even when resource cleanup was not acknowledged.
810
+ children.extend(r for r in error.results if isinstance(r, Sandbox))
811
+ children.extend(error.pending_sandboxes)
812
+ pending_snapshot = error.snapshot_id
813
+ raise
814
+ for result in results:
815
+ if isinstance(result, Exception):
816
+ print("child failed:", result)
817
+ else:
818
+ children.append(result)
819
+ print(result.id, result.files.read("/tmp/state"))
820
+ finally:
821
+ # Production callers should attempt every deletion and retain failed IDs.
822
+ for child in children:
823
+ child.kill()
824
+ if pending_snapshot is not None:
825
+ Sandbox.delete_snapshot(pending_snapshot)
826
+ ```
827
+
828
+ The maintained [`examples/fork.py`](./examples/fork.py) attempts cleanup for all
829
+ known children even when one deletion fails, and preserves workload exceptions.
830
+
831
+ | Parameter | Meaning |
832
+ | --- | --- |
833
+ | `count=1` | Integer 1..20; booleans are rejected. |
834
+ | `idle_timeout=None` | Child idle timeout in seconds; omitted inherits the source's configured idle timeout. Zero disables idle expiry. |
835
+ | `snapshot_timeout=300` | Capture operation deadline in seconds, 1..3600. |
836
+ | `restore_timeout=None` | Logical creation budget per child in seconds; minimum/default is the source's schedule timeout + 60. Checked before capture. |
837
+
838
+ Children inherit captured resources. Fork explicitly supplies the source
839
+ client's last acknowledged network policy with each create request, including
840
+ successful policy updates since source creation. After an uncertain/interrupted
841
+ policy update it refuses to capture until a policy is successfully reapplied.
842
+ Serialize policy changes and captures in the application. This differs from a
843
+ bare `from_snapshot()` call, which requires an explicit policy for restrictions.
844
+ As with restore, the logical create budget excludes client/transport overhead;
845
+ the client's cwd, declared ports, mounts and reverse tunnel are rebound with
846
+ fresh handles. Children are non-detached owned handles even if the source is
847
+ detached. Writes to each child's captured root filesystem and process memory
848
+ are independent. External services and writable HostPath mounts remain shared
849
+ external state. Quiesce model/tool streams before capture, reconnect affected
850
+ connections afterwards, and do not relaunch processes already restored from
851
+ memory. Deleting the source or one child does not delete its siblings.
852
+
853
+ The count/ordered partial-result convention follows
854
+ [E2B fork](https://docs.e2b.dev/sandbox/fork). Deliberate differences:
855
+
856
+ - This release provides an instance method, not E2B's additional fork-by-ID
857
+ class variant. There is no separate all-or-nothing `clone()` operation.
858
+ - The pinned `adx-environment==0.1.0rc3` and AKernel-pinned ADX API have
859
+ snapshot/create/delete primitives but no native batch fork method/route.
860
+ The ADX adapter captures once and then creates children **sequentially**;
861
+ this is multiple requests, not E2B's single server-side fork operation.
862
+ - E2B fork's `timeout` is an absolute child lifetime. AKernel exposes
863
+ `idle_timeout` and named operation deadlines instead of promising a lifetime
864
+ its backend does not implement. There is no ambiguous `timeout` alias.
865
+ - Only a confirmed new environment's creation policy is trusted. Named reuse
866
+ can retain an older specification even when the constructor succeeds; missing
867
+ ownership confirmation and connected handles are unconfirmed too. Fork fails
868
+ with `NETWORK_POLICY_UNCONFIRMED` before capture until the caller successfully
869
+ reapplies the intended policy with `update_network_policy(policy)`. Passing
870
+ constructor arguments again does not prove they changed an existing sandbox.
871
+ - Temporary snapshot deletion is requested after all creates settle. When the
872
+ backend explicitly rejects it with `SNAPSHOT_IN_USE`/`not_started`, each child
873
+ exposes `fork_snapshot_id`; the SDK retains the snapshot and deletes it after
874
+ the last owned child's confirmed `kill()`. `close()` does not release a live
875
+ child's reference. Save the ID if deleting children by ID, from another client,
876
+ or after a client restart; then delete the snapshot explicitly after its users
877
+ are released. No background or garbage-collection deletion is performed.
878
+ Other initial cleanup failures raise `ForkCleanupError`: `results` retains
879
+ successful child handles, `snapshot_id` supports retrying deletion, and
880
+ `cleanup_error` preserves the original error. A failed client binding whose
881
+ child deletion fails or is unconfirmed is retained in `pending_sandboxes`;
882
+ retry `kill()` after deciding that remote deletion is intended.
883
+ Actual cleanup exceptions are in `child_cleanup_errors`; local-only closure
884
+ need not raise one. With no other cleanup error, `cleanup_error.code` is the
885
+ SDK-generated `CLEANUP_UNCONFIRMED` (`outcome="not_started"` for remote
886
+ deletion). If snapshot cleanup
887
+ fails on the last child kill, that call raises `ForkCleanupError` with the
888
+ snapshot ID; retry that child's `kill()` or delete the snapshot explicitly.
889
+ Confirmed child deletion is not repeated during that retry.
890
+
891
+ Unknown capture/create outcomes preserve `BackendOperationError` request and
892
+ lookup identity; the SDK does not initiate an extra logical attempt. An
893
+ unconfirmed create may still exist remotely. Distinguish native lookup names
894
+ from canonical IDs when reconciling. Confirmed-created IDs, including failures
895
+ during native bootstrap or public binding, remain in `environment_id`; rollback
896
+ failures remain in `cleanup_errors`. Interruption, including during final
897
+ snapshot deletion, attempts ownership-aware rollback of every known child and
898
+ deletion of the temporary snapshot, and re-raises the original exception
899
+ object/type. Automatic rollback deletes only confirmed newly created children;
900
+ false or missing ownership closes local resources only. Local closure is not
901
+ remote deletion: these children stay pending with retryable handles, even when
902
+ local closure succeeds. An explicit `kill()` can delete such a child by ID after
903
+ the local client has closed. Reconciliation data
904
+ on that exception includes `snapshot_id`, `created_environment_ids`,
905
+ `pending_environment_ids`, `pending_sandboxes`, `cleanup_errors`, and
906
+ `creation_errors`. Pending sandboxes are cleanup-only public handles: retry
907
+ `kill()` without rebinding commands, files or tunnels. Query or connect separately
908
+ if inspection is needed. Errors from a second interruption during cleanup are
909
+ retained alongside other cleanup errors rather than replacing the first one.
910
+ After releasing unresolved users, explicitly retry snapshot deletion. Unknown
911
+ creation errors retain their lookup/request context; do not blindly delete a
912
+ lookup name as though it were a confirmed-created ID.
913
+ Restore readiness and native session/tunnel initialization can fail before a
914
+ child reaches the fork result list. Secondary interruptions while closing or
915
+ rolling back are retained on the original exception. Known unresolved bootstrap
916
+ children still appear in the fork's pending IDs and cleanup-only handles; their
917
+ failed automatic rollback is not repeated before returning the cancellation.
918
+ `creation_errors` combines the complete backend result set with earlier public
919
+ binding errors, deduplicated by exception identity. It preserves nested request,
920
+ operation, lookup and rollback context even if binding an earlier child is
921
+ interrupted. Uncertain creation errors do not grant ownership or trigger deletion
922
+ of their lookup names.
923
+
924
+ Run real isolation, partial-failure and cleanup acceptance on a supporting
925
+ deployment using a complete image that contains Python 3 (not a CLI mount image):
926
+
927
+ ```bash
928
+ AKERNEL_RUN_INTEGRATION=1 \
929
+ AKERNEL_TEST_IMAGE=python:3.13-slim-bookworm \
930
+ PYTHONPATH=sdk/python \
931
+ python sdk/python/tests/integration/test_fork.py
932
+ ```
933
+
934
+ The companion `tests/integration/test_fork_network.py` covers two independent
935
+ tunnels (default and custom listen ports), private published-port responses,
936
+ creation-time and updated deny policy, and isolated child policy clearing.
937
+ It first verifies an allowed TCP baseline against `AKERNEL_TEST_EGRESS_HOST`
938
+ (default `1.1.1.1`) and `AKERNEL_TEST_EGRESS_PORT` (443); an unreachable baseline
939
+ fails instead of producing a false policy pass. Run both suites with:
940
+
941
+ ```bash
942
+ AKERNEL_RUN_INTEGRATION=1 AKERNEL_TEST_IMAGE=python:3.13-slim-bookworm \
943
+ PYTHONPATH=sdk/python python -m unittest \
944
+ tests.integration.test_fork tests.integration.test_fork_network -v
945
+ ```
946
+
947
+ The partial-failure case injects one client-side error before its child create;
948
+ the other two children are real. The cleanup case injects one deletion failure
949
+ and verifies explicit retry. The suite checks real process memory/PID/open-file
950
+ offsets, private writes, source/sibling independence, and snapshot collection.
951
+ It never changes cluster configuration or induces a platform capacity failure.
952
+
571
953
  ## Local failover and reload
572
954
 
573
955
  `Sandbox(failover=True)` opts into same-node recovery of the same logical
@@ -586,8 +968,9 @@ created by sandbox workloads through Execd's internal `POST /checkpoint`
586
968
  endpoint on `/run/akernel/execd.sock`. A successful request returns
587
969
  `{"status":"completed"}`; a concurrent checkpoint request returns HTTP 409.
588
970
  This Unix-socket protocol is experimental and is not a stable public AKernel
589
- SDK interface. The SDK deliberately does not expose checkpoint identifiers,
590
- restore, list, or delete operations.
971
+ SDK interface. These anonymous checkpoints have no public catalog; use the
972
+ separate reusable snapshot APIs above when you need an independent snapshot ID
973
+ and restoration to a new sandbox.
591
974
 
592
975
  The bundled runsc and Firecracker runtimes support this recovery flow. The
593
976
  maintained example validates it with runsc and installs curl in the sandbox
@@ -708,15 +1091,37 @@ runtime inside an S3 object. When neither source is supplied, AKernel sends
708
1091
  only the selected isolation runtime and openYuanRong overlays it onto the
709
1092
  rootfs configured by the deployed service.
710
1093
 
711
- Firecracker supports `Sandbox(runtime="firecracker", image="ubuntu:24.04")`
712
- with the bundled virtio-fs configuration. Both OCI and Nydus roots use the
713
- image manager's directory directly, without conversion to EROFS. The shared
714
- root stays read-only; sandbox writes use its private ext4 overlay. The deployed
715
- default and explicit `rootfs` S3 objects continue to use raw EROFS images.
716
- The bundled distill-fs supports Nydus RAFS v5. When preparing Nydus images with
717
- `nydusify convert`, select `--fs-version 5` explicitly.
718
- Custom deployments must enable `plugin.runtime.firecracker.virtiofs_enabled`
719
- and install the matching Firecracker stack and virtiofsd.
1094
+ The default Firecracker profile uses the deployed EROFS root when `image`
1095
+ and `rootfs` are omitted; explicit `rootfs` S3 objects must contain raw EROFS
1096
+ images. `Sandbox(runtime="firecracker", image="ubuntu:24.04")` requires the
1097
+ operator-enabled virtio-fs profile described below. With that profile, OCI and
1098
+ Nydus roots use the image manager's directory directly. The shared root stays
1099
+ read-only; sandbox writes use its private node-local ext4 overlay. The bundled
1100
+ distill-fs supports Nydus RAFS v5; select `--fs-version 5` when preparing images
1101
+ with `nydusify convert`.
1102
+
1103
+ ### Enabling Firecracker directory access
1104
+
1105
+ This is a node deployment setting, not an SDK `extra_config` option. Helm and
1106
+ standalone keep `plugin.runtime.firecracker.virtiofs_enabled=false` by default.
1107
+ Operators who need OCI/Nydus roots or HostPath directories must:
1108
+
1109
+ 1. Qualify a KVM-capable image containing the compatible Firecracker VMM,
1110
+ guest kernel/agent and virtiofsd. Include the Firecracker build payload.
1111
+ 2. Drain existing Firecracker sandboxes before changing the runtime profile.
1112
+ Recreate recovery points when the rootfs or mount layout changes.
1113
+ 3. In Helm, update the `[plugin.runtime.firecracker]` section of
1114
+ `node.config.sandboxd.config` to `virtiofs_enabled=true` (`core.node...` in
1115
+ the umbrella chart), preserving the rest of the TOML configuration. For
1116
+ standalone, update the same section in the sandboxd configuration used by
1117
+ the launcher. Restart the drained node services with the updated profile.
1118
+ 4. Verify a fresh Sandbox using the required root or HostPath directory,
1119
+ including permissions, private writes and checkpoint/reload, before
1120
+ admitting workloads. Keep runtime state and private writable layers on
1121
+ node-local disk, separate from shared checkpoints.
1122
+
1123
+ An incompatible runtime or filesystem profile requires new recovery points;
1124
+ do not restore old checkpoints across that migration.
720
1125
 
721
1126
  The same `S3Config` type can be used as a read-only mount source:
722
1127
 
@@ -734,8 +1139,64 @@ OCI images can also be mounted read-only:
734
1139
  mount = Mount(target="/opt/tools", image_url="ubuntu:24.04")
735
1140
  ```
736
1141
 
737
- Firecracker mounts must instead use `type="erofs"` with an S3 object containing
738
- a raw EROFS image; it rejects `image_url` and directory-backed mounts.
1142
+ Firecracker S3 mounts use `type="erofs"` with a raw EROFS image. It rejects
1143
+ `image_url` mounts. HostPath directories require the enabled virtio-fs profile
1144
+ above; the default profile rejects them.
1145
+
1146
+ ### Node HostPath bind mounts
1147
+
1148
+ Trusted management clients can bind an existing node-visible path into a
1149
+ sandbox. For example, expose a prepared log directory on a selected runsc node:
1150
+
1151
+ ```python
1152
+ from akernel_sdk import Mount, Sandbox
1153
+
1154
+ logs = Mount(
1155
+ target="/apps/logs", # Path inside the sandbox.
1156
+ host_path="/apps/logs", # Path visible to sandboxd on the selected node.
1157
+ read_only=False, # Explicitly allow application log writes.
1158
+ )
1159
+ with Sandbox(node_id="<prepared-node-id>", mounts=[logs]) as sandbox:
1160
+ print(sandbox.commands.run("ls -ld /apps/logs").stdout)
1161
+ ```
1162
+
1163
+ `read_only=True` is the default. HostPath supports `type="bind"` only;
1164
+ `read_only=False` is rejected for Image/S3 mounts, which remain read-only.
1165
+ Exactly one of `image_url`, `s3_config`, or `host_path` is required. The source
1166
+ must be an absolute POSIX path without traversal components or control
1167
+ characters. The SDK does not inspect the client machine's filesystem or create
1168
+ the source directory; it sends `create_if_missing=false`. An absent source or
1169
+ insufficient filesystem permissions is a node-side error.
1170
+
1171
+ In Kubernetes, the node Pod must first see the source, for example through an
1172
+ operator-managed `hostPath` volume and matching `volumeMount`. An ECS directory
1173
+ does not become visible to sandboxd merely because its path is in the SDK
1174
+ request. Directory ownership and permissions must allow the sandbox's process
1175
+ UID/GID to use it. Separate workers' local directories are separate data;
1176
+ `node_id` selects the prepared node but does not replicate storage. Data written
1177
+ through a bind can outlive the sandbox. Runtime-specific mount restrictions
1178
+ are enforced by the deployed backend. Firecracker HostPath directories,
1179
+ including read-only directories, require the operator-enabled virtio-fs profile.
1180
+ Read-only regular-file injection can use the default Firecracker profile;
1181
+ writable regular-file mounts are unsupported.
1182
+
1183
+ For operator-managed Kubernetes preparation and the retention/C/R contract, see
1184
+ [node-local application directories](../../deploy/docs/node-local-directories.md).
1185
+
1186
+ Restrict HostPath creation to trusted management clients and source paths
1187
+ controlled by the operator. These mounts do not provide per-tenant source-path
1188
+ isolation; SDK path validation and read-only defaults do not authorize access
1189
+ to the node's filesystem. Server permission errors are returned through the
1190
+ normal `BackendOperationError` fields.
1191
+
1192
+ The maintained example requires an explicit node and source. It lists the
1193
+ read-only mount by default; `--write` creates one uniquely named log file and
1194
+ leaves that file on the node:
1195
+
1196
+ ```bash
1197
+ python sdk/python/examples/hostpath_mounts.py \
1198
+ --node-id <prepared-node-id> --host-path /apps/logs --write
1199
+ ```
739
1200
 
740
1201
  ## Launch from a Dockerfile
741
1202
 
@@ -809,7 +1270,19 @@ sandbox.kill() # closes local clients; remote sandbox remains
809
1270
  Sandbox.delete("worker") # terminates the named remote sandbox
810
1271
  ```
811
1272
 
812
- `sandbox.id` is the physical ID shown by `ak list`. `get_info()` returns a
1273
+ When the backend accepts creation with an existing successful name, it returns
1274
+ that sandbox and its original resource allocation; changed arguments never
1275
+ resize it. Some older deployments reject changed specifications with CONFLICT
1276
+ instead of reusing them. If local initialization fails while reusing a result,
1277
+ the SDK closes
1278
+ its local handle and preserves the original workload. Automatic rollback
1279
+ removes a remote instance only when the server confirms this create owns it.
1280
+ This applies to backend readiness and cwd setup, public facade construction,
1281
+ and Dockerfile COPY, RUN or startup-dispatch failures.
1282
+ Use `get_info()` to inspect the retained allocation, or delete the named
1283
+ sandbox before creating a replacement with a different specification.
1284
+
1285
+ `sandbox.id` (also `sandbox.sandbox_id`) is the logical ID shown by `ak list`. `get_info()` returns a
813
1286
  `SandboxInfo` containing `id`, state, requested CPU, memory, XPU and storage,
814
1287
  and the OCI image when one was configured.
815
1288
 
@@ -850,8 +1323,11 @@ Maintained examples are under [`examples/`](./examples):
850
1323
  - `command_stdin.py`
851
1324
  - `custom_image.py`
852
1325
  - `dockerfile_launch.py`
1326
+ - `snapshots.py`
1327
+ - `lifecycle.py`
853
1328
  - `failover_reload.py`
854
1329
  - `gpu_sandbox.py`
1330
+ - `hostpath_mounts.py`
855
1331
  - `named_sandbox.py`
856
1332
  - `network_policy.py`
857
1333
  - `pty.py`
@@ -875,12 +1351,21 @@ PYTHONPATH=sdk/python \
875
1351
  python -m unittest discover -s sdk/python/tests/integration -t sdk/python -v
876
1352
  ```
877
1353
 
878
- Set `AKERNEL_TEST_RUNTIME=firecracker` and `AKERNEL_TEST_IMAGE=ubuntu:24.04`
879
- to exercise the same suite, including checkpoint/reload, against a virtio-fs
880
- image root. `AKERNEL_TEST_IMAGE` also accepts a Nydus image reference and
881
- enables a check that writes stay private to each sandbox sharing the image.
882
- The test image must provide an Ubuntu/Debian userspace with `apt-get` for the
883
- checkpoint test's curl and CA certificate installation. Omit
1354
+ Set `AKERNEL_TEST_RUNTIME=firecracker` or `firecracker-pvm` and
1355
+ `AKERNEL_TEST_IMAGE=ubuntu:24.04` after explicitly enabling a compatible
1356
+ virtio-fs profile on the test node to exercise the same suite, including
1357
+ checkpoint/reload, against a directory image root. `AKERNEL_TEST_IMAGE` also
1358
+ accepts a Nydus image reference and enables checks for private writes and
1359
+ network policy replacement. Use immutable digest references when qualifying a
1360
+ runtime release, and run both OCI and Nydus images. The pinned distill-fs
1361
+ requires Nydus RAFS v5.
1362
+
1363
+ The network checks use `https://example.com/` by default. Set
1364
+ `AKERNEL_TEST_NETWORK_URL` to an HTTP endpoint reachable from the sandbox to
1365
+ verify an isolated deployment without external network access. The suite checks
1366
+ access before blocking new flows, SDK command execution while blocked, and
1367
+ access after removing the policy. Images can include `curl` and CA certificates
1368
+ in advance; otherwise the suite installs them using `apt-get`. Omit
884
1369
  `AKERNEL_TEST_IMAGE` to test the deployed default EROFS root.
885
1370
 
886
1371
  Load and transfer benchmarks live under [`benchmarks/`](./benchmarks) and are
@@ -896,7 +1381,7 @@ not part of the default test suite.
896
1381
  | `SandboxInfo` | `id`, `state`, `cpu`, `memory`, `image`, `xpu`, `storage_mb` |
897
1382
  | `NodeInfo` | `id`, `status`, `capacity`, `allocatable`, `labels` |
898
1383
  | `S3Config` | `endpoint`, `bucket`, `object`, optional credentials |
899
- | `Mount` | `target`, one source, and `type` |
1384
+ | `Mount` | `target`, exactly one of `image_url` / `s3_config` / `host_path`, `type`, `read_only` (default `True`; writable only for HostPath) |
900
1385
  | `HttpReverseTunnel` | `target`, `reverse_port`, `listen_port`, `connect_timeout` |
901
1386
  | `PortRange` | `first`, `last` |
902
1387
  | `NetworkRule` | action, direction, protocol, peer and sandbox port selectors, priority |
@@ -915,7 +1400,9 @@ not part of the default test suite.
915
1400
  public exception: `code`, `retry`, `outcome`, `request_id`, `operation_id`,
916
1401
  `environment_id`, `status_code`, `scheduling_failure`, `failure`,
917
1402
  `probe_failure`, and `request_forwarded`. Fields absent from the backend response
918
- are `None`; the original exception remains in `__cause__`.
1403
+ are `None`; the original exception remains in `__cause__`. `cleanup_errors` is a
1404
+ tuple of rollback failures (empty when absent). Known-created IDs and these
1405
+ errors are also attached to interruption exceptions without changing their type.
919
1406
 
920
1407
  ```python
921
1408
  from akernel_sdk import BackendOperationError, Sandbox