Skip to content

feat(go): add box archive import and export - #1162

Merged
DorianZheng merged 1 commit into
mainfrom
codex/go-sdk-export-import-runner-test
Aug 8, 2026
Merged

feat(go): add box archive import and export#1162
DorianZheng merged 1 commit into
mainfrom
codex/go-sdk-export-import-runner-test

Conversation

@ltstriker

@ltstriker ltstriker commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Expose the existing box archive export and import capabilities through the public C API and Go SDK. Preserve caller ownership of archive files and provide an opt-in real-VM round-trip test.

Call graph

Before
LiteBox::export (LiteBox · src/boxlite/src/litebox/mod.rs:193) — Rust archive export with no Go SDK entry point
Runtime::import_box (Runtime · src/boxlite/src/runtime/rt_impl.rs:382) — Rust archive import with no Go SDK entry point

After
(*Box).Export (Box · sdks/go/archive.go:61) — validates input and submits export
└─ boxlite_box_export (public C FFI · sdks/c/src/archive.rs:51) — invokes LiteBox::export asynchronously
└─ LiteBox::export (LiteBox · src/boxlite/src/litebox/mod.rs:193) — writes the caller-owned archive
└─ dispatch_event (Runtime event queue · sdks/c/src/runtime.rs:527) — delivers the archive path to Go
(*Runtime).Import (Runtime · sdks/go/archive.go:127) — maps an empty name to None and submits import
└─ boxlite_runtime_import (public C FFI · sdks/c/src/archive.rs:67) — invokes Runtime::import_box asynchronously
└─ Runtime::import_box (Runtime · src/boxlite/src/runtime/rt_impl.rs:382) — creates a new stopped box
└─ dispatch_event (Runtime event queue · sdks/c/src/runtime.rs:527) — delivers the owned box handle to Go

Changes

  • Add Box.Export and Runtime.Import with input validation and callback-backed result delivery.
  • Expose archive export/import entrypoints and callback types through the public C header; the Go SDK uses those same entrypoints.
  • Reclaim undelivered archive callback payloads and queued native events.
  • Reject Local imports after runtime shutdown while preserving Local export and REST runtime behavior.
  • Document trusted local import and caller-managed archive lifetime.
  • Add Go and Rust FFI coverage plus a standalone real-VM Go archive integration target.

How to verify

  • make test:unit:go FILTER='Archive|AbandonOwnedResult'
  • GOFLAGS=-race make test:unit:go FILTER='Archive|AbandonOwnedResult'
  • make test:unit:ffi
  • make test:unit:rust FILTER='import_after_shutdown'
  • make test:unit:go
  • make test:unit:core
  • make fmt:check
  • make lint
  • make test:integration:go FILTER=TestIntegrationArchiveRoundTrip

Risks / rollout

  • Local runtimes trust caller-provided archives by design; REST uploads continue to use the server's untrusted-upload policy.

Summary by CodeRabbit

  • New Features

    • Added Go SDK support for exporting boxes to archives and importing archived boxes into a runtime.
    • Added corresponding C SDK APIs for asynchronous archive export and import.
    • Imported boxes start stopped, receive a new ID, and preserve durable data.
    • Added documentation and examples covering archive workflows, cleanup, and trust requirements.
  • Bug Fixes

    • Improved handling of cancellation, runtime shutdown, invalid inputs, and asynchronous callback resources.
  • Tests

    • Added comprehensive unit and integration coverage for archive round trips and error handling.

Copilot AI lite review requested due to automatic review settings August 7, 2026 08:40
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds asynchronous archive export and import to the Go SDK through new C and Rust FFI APIs. It adds callback ownership handling, cancellation and shutdown behavior, documentation, unit tests, and a VM-backed round-trip integration test.

Changes

Go archive SDK

Layer / File(s) Summary
Native archive bridge
sdks/c/include/boxlite.h, sdks/c/src/archive.rs, sdks/c/src/event_queue.rs, sdks/c/src/runtime.rs, sdks/c/src/lib.rs
The C SDK exposes asynchronous archive export and trusted import. Runtime events carry archive paths and imported box handles. Validation, conversion, callback dispatch, and ownership tests cover the new APIs.
Go API and callback bridge
sdks/go/archive.go, sdks/go/bridge.*, sdks/go/bridge_callback.go, sdks/go/archive_test.go, sdks/go/README.md
The Go SDK adds Box.Export and Runtime.Import. The bridge translates callbacks and releases native payloads. Tests cover validation, cancellation, abandoned results, callback ownership, and runtime closure.
Archive operation lifecycle
sdks/go/runtime.go, src/boxlite/src/runtime/rt_impl.rs, sdks/c/src/event_queue.rs
Event queues reclaim pending payloads during closure and reject late enqueues. Runtime imports return Stopped after shutdown.
Round-trip integration validation
sdks/go/integration/archive/archive_test.go, make/test.mk, make/help.mk
The VM integration test validates export, import, archive retention, restored durability, restored writes, and shutdown behavior. Make help documents the dedicated test target and filter option.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels: e2e-local

Suggested reviewers: dorianzheng

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding Go box archive import and export support.
Description check ✅ Passed The description includes the required summary, call graph, changes, verification steps, and rollout risks with relevant implementation details.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/go-sdk-export-import-runner-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR exposes BoxLite’s existing box archive export/import functionality through the Go SDK, wiring Go-facing Box.Export / Runtime.Import to new Go-private C/FFI bridge entry points, and adding unit + opt-in real-VM integration coverage. It also coordinates runtime teardown with in-flight archive operations so Go callers don’t lose callbacks when Runtime.Close / Runtime.Shutdown runs.

Changes:

  • Add Go SDK APIs: (*Box).Export and (*Runtime).Import, including argument validation and “check ctx before submission; always await committed callback” semantics.
  • Add Go-private bridge surface (sdks/go/bridge.h, callback accessors, Go callback dispatch) and Rust C SDK support for string-returning export events + import handle events.
  • Add targeted Go unit tests and an opt-in VM-backed integration round-trip test, plus make targets for running that integration test.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
sdks/go/runtime.go Adds an archive operation gate and integrates it into Close/Shutdown sequencing.
sdks/go/README.md Documents archive export/import usage and archive lifetime semantics.
sdks/go/integration/archive/archive_test.go Adds a VM-backed round-trip archive integration test (opt-in build tags).
sdks/go/bridge.h Declares Go-private C bridge callbacks + export/import entry points.
sdks/go/bridge.c Switches to bridge.h and adds callback accessor functions for Go archive callbacks.
sdks/go/bridge_callback.go Adds Go dispatch callbacks for export (string payload) and import (box handle payload).
sdks/go/archive.go Introduces Box.Export and Runtime.Import implementations using the new bridge functions.
sdks/go/archive_test.go Adds unit tests for validation, cancellation behavior, and teardown waiting semantics.
sdks/c/src/runtime.rs Adds string-event dispatch helper and tests ownership transfer for export/import events.
sdks/c/src/lib.rs Registers the new archive module in the C SDK crate.
sdks/c/src/event_queue.rs Adds new runtime event variants for Go export/import completions.
sdks/c/src/archive.rs Implements the Go-private C symbols to async-export and import archives and post results via the event queue.
sdks/c/cbindgen.toml Excludes Go-private bridge types/symbols from the public generated C header.
make/test.mk Adds test:integration:go target for the archive round-trip test.
make/help.mk Documents the new Go integration test make target.
Suppressed comments (1)

sdks/go/archive.go:130

  • Import returns ErrRuntimeClosed when the archive gate is closed. Because Runtime.Shutdown closes the same gate (sdks/go/runtime.go:154), this can yield ErrRuntimeClosed after Shutdown, which is inconsistent with other operations that report ErrStopped for a shut down runtime. Consider returning an ErrStopped-coded *Error (or letting the native layer return ErrStopped) when the gate is closed due to shutdown, reserving ErrRuntimeClosed for Runtime.Close races.
	if !r.archiveOperations.tryAcquire() {
		return nil, ErrRuntimeClosed
	}

Comment thread sdks/go/archive.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdks/go/runtime.go (1)

135-150: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close can now double-free the native runtime under concurrent calls.

Close checks r.handle == nil, then clears r.handle at the end. No lock protects that read-modify-write. Two concurrent Close calls can both pass the nil check and both call C.boxlite_runtime_free(r.handle).

The new closeAndWait call makes this reachable in practice. Close now blocks for the full duration of an in-flight archive operation. A second Close on another goroutine enters the same window and blocks on the same gate. When the archive operation finishes, closeAndWait returns on both goroutines and both proceed to free the handle. Before this change, Close did not block, so the window was very small.

Guard the teardown with a sync.Once, or take a mutex around the handle check and the free.

🔒️ Proposed fix
 type Runtime struct {
 	handle *C.CBoxliteRuntime
 
+	closeOnce sync.Once
 	drainOnce sync.Once
 func (r *Runtime) Close() error {
-	if r.handle == nil {
-		return nil
-	}
-
-	r.archiveOperations.closeAndWait()
-	r.closingOnce.Do(func() {
-		if r.closing != nil {
-			close(r.closing)
-		}
-	})
-	r.stopDrain()
-	C.boxlite_runtime_free(r.handle)
-	r.handle = nil
+	r.closeOnce.Do(func() {
+		if r.handle == nil {
+			return
+		}
+		r.archiveOperations.closeAndWait()
+		r.closingOnce.Do(func() {
+			if r.closing != nil {
+				close(r.closing)
+			}
+		})
+		r.stopDrain()
+		C.boxlite_runtime_free(r.handle)
+		r.handle = nil
+	})
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/runtime.go` around lines 135 - 150, Make Runtime.Close
concurrency-safe by guarding the handle nil check and native teardown with a
sync.Once or mutex. Ensure only one caller executes archive shutdown, stopDrain,
C.boxlite_runtime_free, and sets r.handle to nil, while concurrent callers
return without repeating the free.
🧹 Nitpick comments (10)
sdks/go/bridge_callback.go (1)

113-151: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The new callbacks free payloads more thoroughly than the existing ones.

goBoxliteOnExportBox and goBoxliteOnImportBox release the payload on the h == 0 path and on the channel type-mismatch path. goBoxliteOnCreateBox at Line 97 and goBoxliteOnGetBox at Line 183 return on those same two paths without freeing, so they leak a CBoxHandle.

The new code is correct. The older callbacks should adopt the same pattern in a follow-up change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/bridge_callback.go` around lines 113 - 151, Update
goBoxliteOnCreateBox and goBoxliteOnGetBox to free their CBoxHandle payloads on
both the h == 0 path and the channel type-mismatch path, matching the cleanup
behavior in goBoxliteOnImportBox. Use the existing freeBoxHandlePayload helper
before each early return while preserving the current callback flow.
sdks/c/src/archive.rs (2)

256-256: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why the dangling pointers are safe in these tests.

Each test passes a dangling BoxHandle or RuntimeHandle pointer. The tests pass only because every exercised path returns before export_box and import_box reach &*handle and &*runtime. That coupling is invisible at the call site. If a later change moves the dereference above the argument checks, these tests trigger undefined behavior instead of failing an assertion.

Add a short comment that records the invariant.

♻️ Suggested comment
     fn export_rejects_invalid_arguments_synchronously() {
         let dest = CString::new("/tmp/export.boxlite").unwrap();
+        // Safe only because every case below returns during argument
+        // validation, before `export_box` dereferences the handle.
         let dangling_handle = ptr::NonNull::<BoxHandle>::dangling().as_ptr();

Also applies to: 296-296, 339-340

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/c/src/archive.rs` at line 256, Add a short safety comment beside the
dangling BoxHandle and RuntimeHandle test pointers, including the instances
around dangling_handle, documenting that exercised export_box/import_box paths
validate arguments and return before dereferencing them; preserve the existing
test setup.

24-24: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add test coverage for the C SDK bridge signatures.

sdks/c uses edition.workspace = true ("2024") and rust-version = "1.88", so #[unsafe(no_mangle)] is supported; BoxliteErrorCode is #[repr(C)] with explicit discriminants, matching the manual enum BoxliteErrorCode header return type. The remaining risk is that sdks/go/bridge.h declares only the cboxbridge.c FFI symbols, while cbindgen.toml excludes these functions from generated headers. Add a signature-invariant check so future parameter/return-type changes cannot drift across the C/Go boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/c/src/archive.rs` at line 24, Add a signature-invariant test for the C
SDK bridge, covering the FFI symbols declared in sdks/go/bridge.h and
implemented by cboxbridge.c, including parameter and return types. Keep the
check aligned with the manually maintained header and excluded cbindgen
functions so future Rust bridge signature changes cannot drift across the C/Go
boundary.
sdks/c/src/runtime.rs (1)

946-970: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the RuntimeEvent::GoImportBox arm end to end.

go_import_dispatch_transfers_handle_ownership_to_callback calls dispatch_handle_event directly with a stand-in type. It proves generic ownership transfer. It does not prove that the new match arm at Line 560 routes GoImportBox to the handle dispatcher. A small test that constructs the event and calls dispatch_event would close that gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/c/src/runtime.rs` around lines 946 - 970, Add an end-to-end test for the
RuntimeEvent::GoImportBox branch that constructs the event with an owned import
handle and invokes dispatch_event, using the existing consume_go_import_handle
callback and drop counter to verify routing and ownership transfer. Keep the
generic dispatch_handle_event test unchanged.
sdks/go/archive.go (1)

92-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting the unbounded wait on the native callback.

completed := <-result has no escape path. If the native bridge never queues the completion event, Export or Import blocks forever. Because the archive gate holds Close and Shutdown, a lost callback also blocks runtime teardown forever. The current design is intentional and keeps the drain loop alive, so no change is required now. Add a short note in the code that a lost native event blocks teardown, so future maintainers understand the coupling.

Also applies to: 162-162

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/archive.go` at line 92, Add a concise comment beside the blocking
receives in Export and Import explaining that a lost native completion event
causes the operation and runtime teardown to wait indefinitely because the
archive gate remains held. Preserve the existing unbounded wait and drain-loop
behavior.
make/test.mk (1)

351-356: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the dependency style, and reconsider the test:all:go name.

Two points on this target:

  1. The prerequisite style differs from every neighboring target. test:unit:go and test:integration:c invoke @$(MAKE) dev:go and @$(MAKE) dev:c inside the recipe. test:integration:rust and test:integration:cli use the $(if $(SETUP_DONE),,...) guard so a parent target does not rebuild dependencies. This target uses a plain dev\:go prerequisite, which skips the SETUP_DONE guard. Pick one of the two existing styles.

  2. test:all:go at line 359 still runs only test:unit:go. The name now understates the suite, because a second Go suite exists. Keeping the integration test out of the default matrices is intentional, per the comment at lines 351-352. Consider renaming the help text for test:all:go, or state in its comment that the archive integration test is excluded on purpose.

♻️ Proposed change for point 1
 # Go SDK archive round-trip integration test. Intentionally excluded from
 # default test matrices and CI.
-test\:integration\:go: dev\:go
+test\:integration\:go:
 	`@echo` "🧪 Running Go SDK archive integration test (requires VM)..."
+	@$(MAKE) dev:go
 	`@cd` sdks/go && go test -count=1 -tags=boxlite_dev,boxlite_integration -v $(GOTEST_FILTER) ./integration/archive
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@make/test.mk` around lines 351 - 356, Align test:integration:go with
neighboring targets by using either the in-recipe @$(MAKE) dev:go dependency
style or the existing SETUP_DONE guard, rather than a plain prerequisite. Also
clarify test:all:go’s scope by updating its help text or comment to explicitly
state that it runs only the unit suite and intentionally excludes
test:integration:go.
sdks/go/archive_test.go (1)

154-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split the duplicated preparation-cancellation subtests into one shared helper.

This function spans 101 lines, which exceeds the 100-line limit for files under sdks/. The Import and Export subtests also duplicate the same sequence: build a server, create the REST runtime, lock the archive gate, sample the context, cancel, unlock, and assert. Extract a shared helper that takes the server constructor and the operation closure, in the same style as testArchiveTeardownWaits. This removes the duplication and brings the function under the limit.

As per path instructions: "Limit code blocks to a maximum of 100 lines" and "Keep functions focused and cohesive - each function should do one thing well".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/archive_test.go` around lines 154 - 254, Split
TestArchiveCancellationDuringPreparationPreventsNativeSubmission into a shared
helper, following the pattern of testArchiveTeardownWaits. Have the helper
accept the server-construction callback and archive-operation callback,
centralize runtime setup, archive gate locking, context sampling/cancellation,
unlocking, result validation, and assertArchiveRequestNotSubmitted, then reduce
the Import and Export subtests to configure and invoke it while preserving their
existing operation-specific setup and cleanup.

Source: Path instructions

sdks/go/runtime.go (1)

55-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider creating idle eagerly instead of lazily.

The lazy creation in closeAndWait is correct, because release and closeAndWait both hold mu, and closeAndWait re-reads g.active after it creates the cond. The correctness therefore depends on a non-obvious ordering argument, and the g.idle != nil guard in release looks like a missed wakeup at first reading. Add a cond() accessor that creates the cond under mu, and use it in both methods. This keeps the zero-value archiveOperationGate usable while removing the nil guard.

♻️ Proposed refactor
+// cond returns the idle condition variable. The caller must hold g.mu.
+// The gate is usable as a zero value, so the cond is created on first use.
+func (g *archiveOperationGate) cond() *sync.Cond {
+	if g.idle == nil {
+		g.idle = sync.NewCond(&g.mu)
+	}
+	return g.idle
+}
+
 func (g *archiveOperationGate) release() {
 	g.mu.Lock()
 	defer g.mu.Unlock()
 
 	if g.active == 0 {
 		panic("boxlite: archive operation gate released without an active operation")
 	}
 	g.active--
-	if g.active == 0 && g.idle != nil {
-		g.idle.Broadcast()
+	if g.active == 0 {
+		g.cond().Broadcast()
 	}
 }
 
 func (g *archiveOperationGate) closeAndWait() {
 	g.mu.Lock()
 	defer g.mu.Unlock()
 
 	g.closed = true
-	if g.idle == nil {
-		g.idle = sync.NewCond(&g.mu)
-	}
 	for g.active != 0 {
-		g.idle.Wait()
+		g.cond().Wait()
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/runtime.go` around lines 55 - 67, Refactor archiveOperationGate by
adding a cond() accessor that initializes and returns idle while holding mu,
then use it in both release and closeAndWait. Remove the idle nil guard in
release while preserving zero-value usability and the existing close-and-wait
synchronization behavior.
sdks/go/integration/archive/archive_test.go (2)

18-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split this test into phase helpers.

TestIntegrationArchiveRoundTrip spans 182 lines, which exceeds the 100-line limit for files under sdks/. The body also performs several distinct jobs: create and seed the source box, export and validate the archive, import and validate box identity and state, and verify the restored box after archive deletion. Extract each phase into a named helper, for example exportAndValidateArchive, assertRestoredIdentity, and assertRestoredBoxUsable. The test then reads as a sequence of phases and stays under the limit.

As per path instructions: "Limit code blocks to a maximum of 100 lines" and "Keep functions focused and cohesive - each function should do one thing well".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/integration/archive/archive_test.go` around lines 18 - 199, The
TestIntegrationArchiveRoundTrip function is too long and combines multiple
responsibilities. Extract cohesive phase helpers for creating/seeding the source
box, exporting and validating the archive, validating restored identity/state,
and verifying restored usability after archive deletion; keep
TestIntegrationArchiveRoundTrip as a short sequence invoking those helpers while
preserving shared cleanup and assertions.

Source: Path instructions


201-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move ctx to the first parameter of assertExec.

Go convention and common linters expect ctx context.Context first. assertExec currently puts ctx third, after t and box, and there are four call sites that also need to pass ctx first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/integration/archive/archive_test.go` around lines 201 - 209, Update
the assertExec function signature to place ctx context.Context immediately after
t, before box and the remaining arguments. Adjust all four assertExec call sites
to pass ctx as the first argument after t, preserving the existing operation,
output, command, and args values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdks/go/runtime.go`:
- Around line 153-154: Decide and enforce the intended Shutdown contract in
Runtime.Shutdown: if shutdown is terminal, document that explicitly; otherwise
replace archiveOperations.closeAndWait with a non-terminal idle-drain mechanism
such as waitIdle, or restore admission on every native-submission and
context-cancellation error path before returning. Ensure failed or canceled
Shutdown calls leave Export and Import usable when the runtime handle remains
valid.

---

Outside diff comments:
In `@sdks/go/runtime.go`:
- Around line 135-150: Make Runtime.Close concurrency-safe by guarding the
handle nil check and native teardown with a sync.Once or mutex. Ensure only one
caller executes archive shutdown, stopDrain, C.boxlite_runtime_free, and sets
r.handle to nil, while concurrent callers return without repeating the free.

---

Nitpick comments:
In `@make/test.mk`:
- Around line 351-356: Align test:integration:go with neighboring targets by
using either the in-recipe @$(MAKE) dev:go dependency style or the existing
SETUP_DONE guard, rather than a plain prerequisite. Also clarify test:all:go’s
scope by updating its help text or comment to explicitly state that it runs only
the unit suite and intentionally excludes test:integration:go.

In `@sdks/c/src/archive.rs`:
- Line 256: Add a short safety comment beside the dangling BoxHandle and
RuntimeHandle test pointers, including the instances around dangling_handle,
documenting that exercised export_box/import_box paths validate arguments and
return before dereferencing them; preserve the existing test setup.
- Line 24: Add a signature-invariant test for the C SDK bridge, covering the FFI
symbols declared in sdks/go/bridge.h and implemented by cboxbridge.c, including
parameter and return types. Keep the check aligned with the manually maintained
header and excluded cbindgen functions so future Rust bridge signature changes
cannot drift across the C/Go boundary.

In `@sdks/c/src/runtime.rs`:
- Around line 946-970: Add an end-to-end test for the RuntimeEvent::GoImportBox
branch that constructs the event with an owned import handle and invokes
dispatch_event, using the existing consume_go_import_handle callback and drop
counter to verify routing and ownership transfer. Keep the generic
dispatch_handle_event test unchanged.

In `@sdks/go/archive_test.go`:
- Around line 154-254: Split
TestArchiveCancellationDuringPreparationPreventsNativeSubmission into a shared
helper, following the pattern of testArchiveTeardownWaits. Have the helper
accept the server-construction callback and archive-operation callback,
centralize runtime setup, archive gate locking, context sampling/cancellation,
unlocking, result validation, and assertArchiveRequestNotSubmitted, then reduce
the Import and Export subtests to configure and invoke it while preserving their
existing operation-specific setup and cleanup.

In `@sdks/go/archive.go`:
- Line 92: Add a concise comment beside the blocking receives in Export and
Import explaining that a lost native completion event causes the operation and
runtime teardown to wait indefinitely because the archive gate remains held.
Preserve the existing unbounded wait and drain-loop behavior.

In `@sdks/go/bridge_callback.go`:
- Around line 113-151: Update goBoxliteOnCreateBox and goBoxliteOnGetBox to free
their CBoxHandle payloads on both the h == 0 path and the channel type-mismatch
path, matching the cleanup behavior in goBoxliteOnImportBox. Use the existing
freeBoxHandlePayload helper before each early return while preserving the
current callback flow.

In `@sdks/go/integration/archive/archive_test.go`:
- Around line 18-199: The TestIntegrationArchiveRoundTrip function is too long
and combines multiple responsibilities. Extract cohesive phase helpers for
creating/seeding the source box, exporting and validating the archive,
validating restored identity/state, and verifying restored usability after
archive deletion; keep TestIntegrationArchiveRoundTrip as a short sequence
invoking those helpers while preserving shared cleanup and assertions.
- Around line 201-209: Update the assertExec function signature to place ctx
context.Context immediately after t, before box and the remaining arguments.
Adjust all four assertExec call sites to pass ctx as the first argument after t,
preserving the existing operation, output, command, and args values.

In `@sdks/go/runtime.go`:
- Around line 55-67: Refactor archiveOperationGate by adding a cond() accessor
that initializes and returns idle while holding mu, then use it in both release
and closeAndWait. Remove the idle nil guard in release while preserving
zero-value usability and the existing close-and-wait synchronization behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc52b850-c1bf-4cc9-b2f3-eac8d5fb7b1d

📥 Commits

Reviewing files that changed from the base of the PR and between 858104a and a6be08b.

📒 Files selected for processing (15)
  • make/help.mk
  • make/test.mk
  • sdks/c/cbindgen.toml
  • sdks/c/src/archive.rs
  • sdks/c/src/event_queue.rs
  • sdks/c/src/lib.rs
  • sdks/c/src/runtime.rs
  • sdks/go/README.md
  • sdks/go/archive.go
  • sdks/go/archive_test.go
  • sdks/go/bridge.c
  • sdks/go/bridge.h
  • sdks/go/bridge_callback.go
  • sdks/go/integration/archive/archive_test.go
  • sdks/go/runtime.go

Comment thread sdks/go/runtime.go Outdated
@DorianZheng
DorianZheng marked this pull request as ready for review August 7, 2026 12:37
@DorianZheng
DorianZheng requested a review from a team as a code owner August 7, 2026 12:37
@boxlite-agent

boxlite-agent Bot commented Aug 7, 2026

Copy link
Copy Markdown

📦 BoxLite review — couldn't complete

claude exited 1

stdout:
{"is_error":true,"duration_api_ms":0,"num_turns":1,"stop_reason":"stop_sequence","session_id":"9d90af73-9005-4747-ab0d-1304cafb82d3","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"api_error","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subtype":"success","api_error_status":403,"result":"Your organization has disabled Claude subscription access for Claude Code · Use an Anthropic API key instead, or ask your admin to enable access","type":"result","duration_ms":930,"uuid":"8818b9f2-0c9f-4822-8184-565449f29c09"}

stderr:
<empty>

powered by BoxLite

Copilot AI review requested due to automatic review settings August 8, 2026 10:23
@ltstriker
ltstriker force-pushed the codex/go-sdk-export-import-runner-test branch from a6be08b to e4dc2e4 Compare August 8, 2026 10:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (2)

sdks/c/src/archive.rs:23

  • The TokioRuntimeDropGuard docs are currently hard to read (lowercase start, sentence fragments, trailing whitespace). This makes it unclear why the guard exists and when it’s needed.
// use it to prevent the runtime from being dropped while the import task is running. 
// The runtime is cloned into the task, 
// and this guard will drop that clone when the task completes.

sdks/c/src/archive.rs:42

  • TokioRuntimeDropGuard::drop spawns a new OS thread just to drop(runtime) when Arc::try_unwrap fails. That path isn’t the final Arc ref, so thread creation is unnecessary overhead and makes teardown harder to reason about.
        match Arc::try_unwrap(runtime) {
            Ok(runtime) => runtime.shutdown_background(),
            Err(runtime) => {
                drop(std::thread::spawn(move || drop(runtime)));
            }
        }

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/boxlite/src/runtime/rt_impl.rs (1)

378-391: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record the post-shutdown contract in the doc comment.

import_box now rejects requests after shutdown, but export keeps working after shutdown. The Go integration test at sdks/go/integration/archive/archive_test.go lines 201-225 depends on exactly this asymmetry. State the rejection in the doc comment so callers do not have to read the body to learn it.

📝 Proposed doc addition
     /// Import a box from a `.boxlite` archive.
     ///
     /// Creates a new box with a new ID from archived disk images and
     /// configuration. The imported box starts in `Stopped` state.
+    ///
+    /// Returns [`BoxliteError::Stopped`] if the runtime has been shut down.
+    /// The check runs before archive validation.
     pub async fn import_box(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/runtime/rt_impl.rs` around lines 378 - 391, Update the doc
comment for import_box to explicitly state that it rejects calls after the
runtime has been shut down, while preserving the documented import behavior and
distinguishing it from export’s post-shutdown behavior.
sdks/c/src/archive.rs (2)

285-285: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why a dangling handle is safe in these tests.

The tests pass ptr::NonNull::dangling().as_ptr() as the handle and runtime arguments. This is safe only because export_box and import_box validate dest, archive_path, name_or_null, and cb before they dereference the pointer. If a future change moves let handle_ref = &*handle; above the argument validation, these tests become undefined behavior instead of failing assertions. Add a comment that records this ordering dependency.

Also applies to: 368-369

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/c/src/archive.rs` at line 285, Add a comment beside the dangling handle
setup in the affected tests explaining that it is safe only because export_box
and import_box validate dest, archive_path, name_or_null, and cb before
dereferencing handle or runtime. Explicitly document that moving handle_ref =
&*handle or equivalent dereferences before validation would make these tests
undefined behavior.

32-44: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

The Err branch of the guard cannot drop the runtime.

Arc::try_unwrap returns Err only when another Arc clone still exists. Dropping the returned clone therefore only decrements the reference count. It never runs the Runtime destructor. The spawned OS thread performs no useful work and costs one thread per completed import.

♻️ Proposed simplification
 impl Drop for TokioRuntimeDropGuard {
     fn drop(&mut self) {
         let Some(runtime) = self.0.take() else {
             return;
         };
-        match Arc::try_unwrap(runtime) {
-            Ok(runtime) => runtime.shutdown_background(),
-            Err(runtime) => {
-                drop(std::thread::spawn(move || drop(runtime)));
-            }
-        }
+        // Only the last clone can run the runtime destructor, and dropping a
+        // runtime from inside its own worker thread would block. Hand it to
+        // the background shutdown path instead.
+        if let Ok(runtime) = Arc::try_unwrap(runtime) {
+            runtime.shutdown_background();
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/c/src/archive.rs` around lines 32 - 44, Remove the spawned-thread
handling from TokioRuntimeDropGuard::drop. In the Arc::try_unwrap error branch,
directly drop the returned Arc so this guard only releases its reference; allow
the Runtime destructor to run when the final Arc owner is dropped.
sdks/go/integration/archive/archive_test.go (1)

228-236: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move ctx ahead of operation in the signature.

Go places context.Context first, or directly after *testing.T in a test helper. Here it sits third, after box. Several linters that check context placement flag this. Reordering to assertExec(t, ctx, box, operation, wantStdout, command, args...) also makes the four adjacent string parameters easier to read at each call site.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/integration/archive/archive_test.go` around lines 228 - 236, Reorder
the parameters of assertExec so ctx follows t and precedes box, operation,
wantStdout, command, and args. Update every assertExec call site to use the new
order while preserving all argument values and behavior.
sdks/go/archive_test.go (1)

569-627: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the two archive test servers.

newArchiveImportServer and newArchiveExportServer duplicate the /v1/config, /v1/boxes/source-box, and export route handlers. They differ only in which route signals started and blocks on release, and in the capability flags returned by /v1/config. One helper that takes the blocking route removes about 30 duplicated lines and keeps the two fixtures from drifting.

♻️ Sketch of a single parameterized helper
// newArchiveServer serves the REST routes the archive tests need. The handler
// for blockPath signals started and then waits for release.
func newArchiveServer(t *testing.T, blockPath string, started chan<- struct{}, release <-chan struct{}) *httptest.Server {
	t.Helper()
	var startOnce sync.Once

	gate := func(path string) {
		if path == blockPath {
			startOnce.Do(func() { close(started) })
			<-release
		}
	}

	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/v1/config":
			w.Header().Set("Content-Type", "application/json")
			_, _ = io.WriteString(w, `{"capabilities":{"import_enabled":true,"export_enabled":true}}`)
		case r.Method == http.MethodGet && r.URL.Path == "/v1/boxes/source-box":
			w.Header().Set("Content-Type", "application/json")
			_, _ = io.WriteString(w, sourceBoxJSON)
		case r.Method == http.MethodPost && r.URL.Path == "/v1/boxes/import":
			gate(r.URL.Path)
			w.Header().Set("Content-Type", "application/json")
			_, _ = io.WriteString(w, importedBoxJSON)
		case r.Method == http.MethodPost && r.URL.Path == "/v1/boxes/source-box/export":
			gate(r.URL.Path)
			w.Header().Set("Content-Type", "application/octet-stream")
			_, _ = io.WriteString(w, "archive-bytes")
		default:
			http.NotFound(w, r)
		}
	}))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/go/archive_test.go` around lines 569 - 627, Replace
newArchiveImportServer and newArchiveExportServer with one parameterized
newArchiveServer helper accepting the blocking route path, started channel, and
release channel. Consolidate the shared /v1/config, source-box, and export
handlers, using a route-matching gate to signal started once and wait on release
only for the selected path; preserve the import and export capability responses
required by their callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdks/c/src/archive.rs`:
- Around line 18-24: Run cargo fmt for the Rust file, focusing on the
TokioRuntimeDropGuard area and the corresponding code near the later reported
location; remove all trailing whitespace from the affected comments, including
both comment blocks, while preserving their content and behavior.

In `@sdks/go/integration/archive/archive_test.go`:
- Around line 18-226: Split TestIntegrationArchiveRoundTrip into focused phase
helpers or t.Run subtests, keeping the existing execution order and assertions
intact. Introduce a small fixture struct to carry source, restored, IDs, and
runtimeIsShutdown between phases, and retain shared setup and cleanup in the
parent test. Separate source creation/seeding, archive export/validation, import
identity/state validation, deleted-archive independence, and post-shutdown
validation so no code block exceeds 100 lines.

In `@sdks/go/runtime.go`:
- Around line 85-87: Update the comment near Close’s shutdown sequence to remove
the claim that parked Export or Import callers release per-call resources before
the drain goroutine and native runtime stop. Describe that closing r.closing
wakes them with ErrRuntimeClosed, while EventQueue::mark_closed safely drains
and drops undelivered owned payloads during native teardown.

---

Nitpick comments:
In `@sdks/c/src/archive.rs`:
- Line 285: Add a comment beside the dangling handle setup in the affected tests
explaining that it is safe only because export_box and import_box validate dest,
archive_path, name_or_null, and cb before dereferencing handle or runtime.
Explicitly document that moving handle_ref = &*handle or equivalent dereferences
before validation would make these tests undefined behavior.
- Around line 32-44: Remove the spawned-thread handling from
TokioRuntimeDropGuard::drop. In the Arc::try_unwrap error branch, directly drop
the returned Arc so this guard only releases its reference; allow the Runtime
destructor to run when the final Arc owner is dropped.

In `@sdks/go/archive_test.go`:
- Around line 569-627: Replace newArchiveImportServer and newArchiveExportServer
with one parameterized newArchiveServer helper accepting the blocking route
path, started channel, and release channel. Consolidate the shared /v1/config,
source-box, and export handlers, using a route-matching gate to signal started
once and wait on release only for the selected path; preserve the import and
export capability responses required by their callers.

In `@sdks/go/integration/archive/archive_test.go`:
- Around line 228-236: Reorder the parameters of assertExec so ctx follows t and
precedes box, operation, wantStdout, command, and args. Update every assertExec
call site to use the new order while preserving all argument values and
behavior.

In `@src/boxlite/src/runtime/rt_impl.rs`:
- Around line 378-391: Update the doc comment for import_box to explicitly state
that it rejects calls after the runtime has been shut down, while preserving the
documented import behavior and distinguishing it from export’s post-shutdown
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74884d73-2728-4813-867b-91a42f47565f

📥 Commits

Reviewing files that changed from the base of the PR and between a6be08b and e4dc2e4.

📒 Files selected for processing (8)
  • sdks/c/src/archive.rs
  • sdks/c/src/event_queue.rs
  • sdks/go/README.md
  • sdks/go/archive.go
  • sdks/go/archive_test.go
  • sdks/go/integration/archive/archive_test.go
  • sdks/go/runtime.go
  • src/boxlite/src/runtime/rt_impl.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • sdks/go/README.md
  • sdks/go/archive.go

Comment thread sdks/c/src/archive.rs
Comment thread sdks/go/integration/archive/archive_test.go
Comment thread sdks/go/runtime.go
Copilot AI review requested due to automatic review settings August 8, 2026 10:37
@ltstriker
ltstriker force-pushed the codex/go-sdk-export-import-runner-test branch from e4dc2e4 to ccabfae Compare August 8, 2026 10:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (1)

sdks/c/src/archive.rs:45

  • TokioRuntimeDropGuard::drop spawns a new OS thread in the common case where the Tokio runtime still has other Arc owners (Arc::try_unwrap fails). That means every Import will typically create an extra thread just to drop an Arc, which is expensive and can exhaust resources under load. The guard can simply hold an Arc clone for the task lifetime and then let it drop normally; runtime shutdown should be handled by the owner that is actually freeing the runtime, not by each import task.
        match Arc::try_unwrap(runtime) {
            Ok(runtime) => runtime.shutdown_background(),
            Err(runtime) => {
                drop(std::thread::spawn(move || drop(runtime)));
            }

Copilot AI review requested due to automatic review settings August 8, 2026 11:47
@ltstriker
ltstriker force-pushed the codex/go-sdk-export-import-runner-test branch from ccabfae to 00a0484 Compare August 8, 2026 11:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (3)

sdks/c/include/boxlite.h:492

  • PR description says the archive bridge symbols should be excluded from the public C header, but boxlite_box_export/boxlite_runtime_import are now declared in sdks/c/include/boxlite.h. Either hide these declarations from the public header (e.g., move them to a Go-only header like sdks/go/bridge.h or guard with an internal macro) or update the PR description to reflect that the C SDK API surface is expanding to include archive import/export.
// Submit a box export.
//
// On success, the callback owns the returned path and must release it with
// `boxlite_free_string`. The archive itself is never deleted by this bridge.
enum BoxliteErrorCode boxlite_box_export(CBoxHandle *handle,
                                         const char *dest,
                                         CBoxExportCb cb,
                                         void *user_data,
                                         CBoxliteError *out_error);

// Submit a trusted archive import.
//
// A null or empty `name_or_null` leaves the new box unnamed. The callback
// owns the returned stopped box handle. The caller retains ownership of the
// archive file; this bridge never removes it.
enum BoxliteErrorCode boxlite_runtime_import(CBoxliteRuntime *runtime,
                                             const char *archive_path,
                                             const char *name_or_null,
                                             CRuntimeImportCb cb,
                                             void *user_data,
                                             CBoxliteError *out_error);

sdks/c/src/archive.rs:42

  • TokioRuntimeDropGuard::drop spawns a new OS thread on the common path where Arc::try_unwrap fails. That will happen whenever other Arc clones exist (which is typical), so each import can spawn a thread just to drop an Arc — unnecessary overhead and potential resource exhaustion under load. Drop the Arc directly in the Err branch (only the Ok branch needs shutdown_background to avoid blocking when this is the last ref).
        match Arc::try_unwrap(runtime) {
            Ok(runtime) => runtime.shutdown_background(),
            Err(runtime) => {
                drop(std::thread::spawn(move || drop(runtime)));
            }
        }

sdks/c/include/boxlite.h:158

  • PR description says the archive bridge symbols should be excluded from the public C header, but this header now publicly defines the archive callback typedefs (CBoxExportCb/CRuntimeImportCb). If these APIs are intended to be Go-internal only, consider moving these typedefs to sdks/go/bridge.h (or guarding them behind an internal-only macro) and keeping the public C SDK surface unchanged. If they are intended to become part of the C SDK, the PR description should be updated accordingly to avoid mismatched expectations.

This issue also appears on line 472 of the same file.

// Box export completion.
typedef void (*CBoxExportCb)(char*, CBoxliteError*, void*);

typedef struct RuntimeHandle CBoxliteRuntime;

// Runtime import completion.
typedef void (*CRuntimeImportCb)(CBoxHandle*, CBoxliteError*, void*);

typedef struct OptionsHandle CBoxliteOptions;

@DorianZheng
DorianZheng added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 8f102f5 Aug 8, 2026
45 checks passed
@DorianZheng
DorianZheng deleted the codex/go-sdk-export-import-runner-test branch August 8, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants