applecontainer: Run / Start / Stop / Remove (PR-C)#61
Conversation
📝 WalkthroughWalkthroughThis PR implements container lifecycle operations (run, start, stop, delete) across a three-layer architecture: a Swift bridge that performs the work, a C shim that dynamically loads it, and Go runtime methods that call through the shim. Mount type handling is updated to flexibly decode wire formats, and comprehensive tests verify the end-to-end flow and error contracts. ChangesApple Container Lifecycle Bridge
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Fourth PR of M6. Replaces the RunContainer / StartContainer /
StopContainer / RemoveContainer stubs with real implementations.
Bridge (Swift):
- lifecycle.swift: ac_run wraps ContainerClient.create with a full
ContainerConfiguration built from a RunSpec JSON payload, kernel
fetched via ClientKernel.getDefaultKernel. ac_start does
bootstrap + process.start in detached mode with idempotent
short-circuit when the snapshot already reports running. ac_stop
wraps ContainerClient.stop with a Go-supplied grace period.
ac_delete wraps ContainerClient.delete with the force flag.
- ac_bridge.h: documented per the PR-B style guide. Notes Apple's
60s sync timeout for these calls (cold first-run needs to fetch
the kernel and init image).
- Mount mapping: bind → Filesystem.virtiofs (matches the design §11.1
finding — virtiofs is identity-permissive). Tmpfs → Filesystem.tmpfs.
Named volumes treated as virtiofs binds for PR-C; real volume
lifecycle is a later PR.
cgo shim (Go side):
- shim.h / shim.c: four new function-pointer slots and wrappers.
Runtime methods (lifecycle_darwin_arm64.go):
- runSpecToWire marshals runtime.RunSpec into the bridge JSON shape.
Drops RunArgs, Privileged, SecurityOpt (intentionally absent on
the wire type so a future caller depending on them fails the
build instead of silently losing the field). Documented in
design §8.
- RunContainer: marshal → ac_run → decode → returns
runtime.Container in Created state.
- StartContainer: idempotent via bridge.
- StopContainer: clamps Timeout to int32 seconds with overflow guard.
- RemoveContainer: RemoveVolumes silently dropped (volume support
deferred).
- mapRunErr / mapLifecycleErr translate Apple's `notFound`
errors into the runtime.{Container,Image}NotFoundError contract.
Inspect path tightened (inspect_darwin_arm64.go):
- containerMount.Type now decodes Apple's enum-with-associated-values
shape (`{"virtiofs":{}}` etc) via a custom mountTypeWire decoder.
Surfaces as runtime.MountBind / MountTmpfs / MountVolume so callers
reason about mounts in Docker-style terms.
- ReadOnly is now derived from the `options` array containing "ro"
(Apple's wire shape, replacing the synthetic boolean field that
doesn't exist in ContainerSnapshot).
Tests (lifecycle_darwin_arm64_test.go):
- TestLifecycle_EndToEnd: Run → Start → Inspect(running) → Start
(idempotent) → Stop → Inspect(stopped) → Remove →
Inspect(notfound). 5s state-poll loop absorbs the apiserver's
async status transitions.
- TestRunContainer_MissingImage: asserts ImageNotFoundError contract
when the caller hasn't pulled the image first.
- TestRunContainer_BindMount: creates a container with a virtiofs
bind and verifies the inspect path round-trips the mount; absorbs
Apple's trailing-slash path normalization via TrimRight.
Test plan:
- `make bridge && go test ./runtime/applecontainer/...` — full
package green
- `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ./runtime/applecontainer/...`
— stub still compiles
- `go vet ./...` clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f82d51c to
36055fe
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
runtime/applecontainer/lifecycle_darwin_arm64_test.go (1)
126-176: 💤 Low valueConsider verifying mount type mapping in the round-trip assertion.
The test verifies Source and Target but doesn't check that the mount type is correctly mapped (virtiofs → bind). Adding a check for
m.Type == string(runtime.MountBind)would strengthen the round-trip verification.🔧 Suggested enhancement
for _, m := range details.Mounts { if m.Target == "/mnt/work" && - strings.TrimRight(m.Source, "/") == strings.TrimRight(hostDir, "/") { + strings.TrimRight(m.Source, "/") == strings.TrimRight(hostDir, "/") && + m.Type == string(runtime.MountBind) { found = true } }🤖 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 `@runtime/applecontainer/lifecycle_darwin_arm64_test.go` around lines 126 - 176, In TestRunContainer_BindMount, extend the mount round-trip check to assert the mount type is mapped to a bind mount by including a comparison against string(runtime.MountBind) (e.g. require m.Type == string(runtime.MountBind)) when scanning details.Mounts for the entry with Target "/mnt/work" and Source matching hostDir; update the found condition to include this type check and adjust the failure message to surface the observed m.Type for easier diagnosis.
🤖 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 `@applecontainer-bridge/Sources/ACBridge/lifecycle.swift`:
- Around line 240-250: The ac_stop wrapper currently calls
runSync(timeoutSeconds: lifecycleTimeoutSeconds) which can return before the
ContainerClient.stop grace period completes; update ac_stop so the runSync
timeout is derived from the requested timeoutSeconds when >0 (e.g. compute a
syncTimeout = max(lifecycleTimeoutSeconds, Int(timeoutSeconds) + smallBuffer) or
clamp/reject excessively large timeoutSeconds), pass that syncTimeout into
runSync, and ensure you handle the Int32→Int conversion and fallback for
non-positive timeoutSeconds (use .default behavior and the normal
lifecycleTimeoutSeconds); use the ac_stop, runSync, lifecycleTimeoutSeconds, and
ContainerStopOptions symbols to locate the changes.
- Around line 213-229: The start path is a check-then-act race: multiple callers
can observe client.get(id:) status != .running and both call
client.bootstrap(...) / process.start(), causing one to fail; make ac_start
idempotent by collapsing "already started" failures into success or serializing
starts per container. Specifically, after calling client.bootstrap(id:...,
stdio:...) and process.start(), catch the error that indicates the container is
already running (or re-check with client.get(id:) on failure) and return
"{\"ok\":true}" for that case; alternatively implement a per-container mutex
around ac_start to serialize calls so only one caller can execute
client.bootstrap/process.start at a time. Ensure you reference and handle
client.get(id:), client.bootstrap(id:...), and process.start() in the fix.
In `@runtime/applecontainer/lifecycle_darwin_arm64.go`:
- Line 79: The cgo bridge calls (C.ac_run_p, C.ac_start_p, C.ac_stop_p,
C.ac_delete_p) are synchronous and currently invoked directly in RunContainer,
StartContainer, StopContainer, and RemoveContainer so ctx cancellation after the
call starts is ignored; fix by performing each C call in a dedicated goroutine
that sends its result (the raw C string) or an error on a result channel, then
in the caller use select to wait for either the result or ctx.Done(); if ctx is
done first return ctx.Err() to the caller, but ensure the background goroutine
still consumes the C result and calls goStringAndFree to release C memory (so no
leaks) even when the API returned early. Include this pattern for functions
RunContainer, StartContainer, StopContainer, RemoveContainer and their
respective C calls C.ac_run_p, C.ac_start_p, C.ac_stop_p, C.ac_delete_p.
- Around line 127-135: The timeout rounding currently adds 999_999_999 to
opts.Timeout.Nanoseconds(), which can overflow; update the rounding in the block
that sets graceSec (the code that reads opts.Timeout.Nanoseconds(), computes
secs, and assigns graceSec) to use safe ceiling division without addition: read
nanos := opts.Timeout.Nanoseconds(), compute secs := (nanos-1)/1_000_000_000 + 1
(ensuring nanos>0), then clamp secs to int64(1<<31-1) and assign graceSec =
int32(secs); adjust the existing secs and clamp logic around that calculation to
prevent negative wraparound.
---
Nitpick comments:
In `@runtime/applecontainer/lifecycle_darwin_arm64_test.go`:
- Around line 126-176: In TestRunContainer_BindMount, extend the mount
round-trip check to assert the mount type is mapped to a bind mount by including
a comparison against string(runtime.MountBind) (e.g. require m.Type ==
string(runtime.MountBind)) when scanning details.Mounts for the entry with
Target "/mnt/work" and Source matching hostDir; update the found condition to
include this type check and adjust the failure message to surface the observed
m.Type for easier diagnosis.
🪄 Autofix (Beta)
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 491f0255-13ae-4cfd-9bcf-45231f9fa340
📒 Files selected for processing (8)
applecontainer-bridge/Sources/ACBridge/lifecycle.swiftapplecontainer-bridge/include/ac_bridge.hruntime/applecontainer/inspect_darwin_arm64.goruntime/applecontainer/lifecycle_darwin_arm64.goruntime/applecontainer/lifecycle_darwin_arm64_test.goruntime/applecontainer/runtime_darwin_arm64.goruntime/applecontainer/shim.cruntime/applecontainer/shim.h
| let snap = try await client.get(id: id) | ||
| // Idempotency: if the container is already running, return | ||
| // success. Matches the CLI's behavior in | ||
| // ContainerStart.swift L60-72 and Docker's "start" on a | ||
| // running container (no-op). | ||
| if snap.status == .running { | ||
| return "{\"ok\":true}" | ||
| } | ||
| // Detached start: no stdio attachment. ProcessIO with | ||
| // detach=true returns [nil,nil,nil] for stdio; we replicate | ||
| // that directly without instantiating ProcessIO. | ||
| let process = try await client.bootstrap( | ||
| id: id, | ||
| stdio: [nil, nil, nil], | ||
| dynamicEnv: [:] | ||
| ) | ||
| try await process.start() |
There was a problem hiding this comment.
Make ac_start idempotent under concurrent callers.
This is a check-then-act sequence against external state: two callers can both observe status != .running and then race in bootstrap() / start(), so one of them can still fail even though the API is documented as a no-op success for already-started containers. Please collapse the backend's "already running/started" path to success, or serialize starts per container.
🤖 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 `@applecontainer-bridge/Sources/ACBridge/lifecycle.swift` around lines 213 -
229, The start path is a check-then-act race: multiple callers can observe
client.get(id:) status != .running and both call client.bootstrap(...) /
process.start(), causing one to fail; make ac_start idempotent by collapsing
"already started" failures into success or serializing starts per container.
Specifically, after calling client.bootstrap(id:..., stdio:...) and
process.start(), catch the error that indicates the container is already running
(or re-check with client.get(id:) on failure) and return "{\"ok\":true}" for
that case; alternatively implement a per-container mutex around ac_start to
serialize calls so only one caller can execute client.bootstrap/process.start at
a time. Ensure you reference and handle client.get(id:),
client.bootstrap(id:...), and process.start() in the fix.
| public func ac_stop(_ idPtr: UnsafePointer<CChar>?, _ timeoutSeconds: Int32) -> UnsafePointer<CChar>? { | ||
| guard let id = readCString(idPtr) else { return dupNullArgErr("id") } | ||
| return runSync(timeoutSeconds: lifecycleTimeoutSeconds) { | ||
| do { | ||
| // Apple's ContainerStopOptions has its own grace-period | ||
| // knob. timeoutSeconds <= 0 uses the API default (5s | ||
| // SIGTERM, then SIGKILL). | ||
| let opts: ContainerStopOptions = timeoutSeconds > 0 | ||
| ? .init(timeoutInSeconds: timeoutSeconds, signal: SIGTERM) | ||
| : .default | ||
| try await ContainerClient().stop(id: id, opts: opts) |
There was a problem hiding this comment.
Don't let the bridge timeout undercut the requested stop grace period.
ContainerStopOptions honors the caller-provided timeoutSeconds, but the outer runSync wait is still hard-capped at 60s. Any grace period above that can make the bridge return a timeout while the daemon is still legitimately waiting to stop the container. Derive the sync timeout from timeoutSeconds (with a small buffer), or reject/clamp larger values before calling into Swift.
🤖 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 `@applecontainer-bridge/Sources/ACBridge/lifecycle.swift` around lines 240 -
250, The ac_stop wrapper currently calls runSync(timeoutSeconds:
lifecycleTimeoutSeconds) which can return before the ContainerClient.stop grace
period completes; update ac_stop so the runSync timeout is derived from the
requested timeoutSeconds when >0 (e.g. compute a syncTimeout =
max(lifecycleTimeoutSeconds, Int(timeoutSeconds) + smallBuffer) or clamp/reject
excessively large timeoutSeconds), pass that syncTimeout into runSync, and
ensure you handle the Int32→Int conversion and fallback for non-positive
timeoutSeconds (use .default behavior and the normal lifecycleTimeoutSeconds);
use the ac_stop, runSync, lifecycleTimeoutSeconds, and ContainerStopOptions
symbols to locate the changes.
| cSpec := C.CString(string(specBytes)) | ||
| defer C.free(unsafe.Pointer(cSpec)) | ||
|
|
||
| raw := goStringAndFree(C.ac_run_p(cSpec)) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Locate and read the file
fd -t f "lifecycle_darwin_arm64.go" -x wc -l {}Repository: crunchloop/devcontainer
Length of output: 122
🏁 Script executed:
# Read the entire file to understand context handling
fd -t f "lifecycle_darwin_arm64.go" -x cat -n {}Repository: crunchloop/devcontainer
Length of output: 9640
🏁 Script executed:
# Search for the cgo function definitions and their usage
rg "C\.(ac_run_p|ac_start_p|ac_stop_p|ac_delete_p)" --context 5 -A 5Repository: crunchloop/devcontainer
Length of output: 2205
🏁 Script executed:
# Check if these C functions are declared elsewhere with their signatures
rg "ac_run_p|ac_start_p|ac_stop_p|ac_delete_p" --type c --type goRepository: crunchloop/devcontainer
Length of output: 1117
Bridge lifecycle calls do not honor context cancellation after execution begins
Each method (RunContainer at line 79, StartContainer at line 107, StopContainer at line 139, and RemoveContainer at line 166) checks ctx.Err() before invoking its respective cgo bridge call (C.ac_run_p, C.ac_start_p, C.ac_stop_p, C.ac_delete_p). However, the cgo function signatures do not accept context parameters, and once the synchronous C call begins, cancellation or deadline expiry will not interrupt it. The operation will run to completion or until the bridge's internal timeout, regardless of caller context state. This violates caller expectations about context propagation and can cause operations to block longer than intended.
🤖 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 `@runtime/applecontainer/lifecycle_darwin_arm64.go` at line 79, The cgo bridge
calls (C.ac_run_p, C.ac_start_p, C.ac_stop_p, C.ac_delete_p) are synchronous and
currently invoked directly in RunContainer, StartContainer, StopContainer, and
RemoveContainer so ctx cancellation after the call starts is ignored; fix by
performing each C call in a dedicated goroutine that sends its result (the raw C
string) or an error on a result channel, then in the caller use select to wait
for either the result or ctx.Done(); if ctx is done first return ctx.Err() to
the caller, but ensure the background goroutine still consumes the C result and
calls goStringAndFree to release C memory (so no leaks) even when the API
returned early. Include this pattern for functions RunContainer, StartContainer,
StopContainer, RemoveContainer and their respective C calls C.ac_run_p,
C.ac_start_p, C.ac_stop_p, C.ac_delete_p.
| var graceSec int32 | ||
| if opts.Timeout > 0 { | ||
| // Round up to whole seconds, clamp to MaxInt32 in the unlikely | ||
| // case of a wildly-large timeout. | ||
| secs := (opts.Timeout.Nanoseconds() + 999_999_999) / 1_000_000_000 | ||
| if secs > 1<<31-1 { | ||
| secs = 1<<31 - 1 | ||
| } | ||
| graceSec = int32(secs) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "lifecycle_darwin_arm64.go" -type fRepository: crunchloop/devcontainer
Length of output: 118
🏁 Script executed:
cat -n ./runtime/applecontainer/lifecycle_darwin_arm64.goRepository: crunchloop/devcontainer
Length of output: 9640
🏁 Script executed:
find . -type f -name "*.go" | xargs grep -l "type StopOptions struct" | head -5Repository: crunchloop/devcontainer
Length of output: 88
🏁 Script executed:
cat -n ./runtime/runtime.go | head -100Repository: crunchloop/devcontainer
Length of output: 4602
🏁 Script executed:
cat -n ./runtime/runtime.go | grep -A 10 "type StopOptions"Repository: crunchloop/devcontainer
Length of output: 438
🏁 Script executed:
python3 << 'EOF'
# Verify the rounding logic
# Original: (nanos + 999_999_999) / 1_000_000_000
# Proposed: (nanos - 1) / 1_000_000_000 + 1
def test_rounding():
test_cases = [
(1, 1), # 1 nanosecond -> 1 second (round up)
(999_999_999, 1), # 999_999_999 nanoseconds -> 1 second (round up)
(1_000_000_000, 1), # Exactly 1 second
(1_000_000_001, 2), # 1s + 1ns -> 2 seconds (round up)
(2_000_000_000, 2), # Exactly 2 seconds
(2_999_999_999, 3), # ~3 seconds -> 3 seconds (round up)
]
for nanos, expected in test_cases:
# Original formula (with potential overflow issues)
result_original = (nanos + 999_999_999) // 1_000_000_000
# Proposed formula
result_proposed = (nanos - 1) // 1_000_000_000 + 1
match = "✓" if result_original == expected == result_proposed else "✗"
print(f"{match} nanos={nanos:>12}: expected={expected}, original={result_original}, proposed={result_proposed}")
# Test overflow scenario
print("\n--- Overflow Analysis ---")
import sys
max_int64 = 2**63 - 1
print(f"int64.MaxValue = {max_int64}")
print(f"int64.MaxValue - 999_999_999 = {max_int64 - 999_999_999}")
print(f"Adding 999_999_999 to (MaxValue - 999_999_999) causes overflow:")
print(f" Original: {(max_int64 - 999_999_999) + 999_999_999} = MaxValue (OK)")
print(f" But in int64: wraps to {(max_int64 - 999_999_999 + 999_999_999) - 2**64} if exceeded")
# In Python, int doesn't overflow, but let's show the conceptual issue
large_nanos = max_int64 - 500_000_000
would_overflow = (large_nanos + 999_999_999)
print(f"\nLarge timeout: {large_nanos} nanoseconds")
print(f"After adding 999_999_999: {would_overflow}")
print(f"In int64, this would overflow and become negative")
test_rounding()
EOFRepository: crunchloop/devcontainer
Length of output: 838
Timeout rounding can overflow before clamping
Line 131: (opts.Timeout.Nanoseconds() + 999_999_999) can overflow for very large positive durations (>~292 years). The addition wraps to a negative int64, which bypasses the clamp check since the condition if secs > 1<<31-1 only guards against positive overflow, not negative wraparound. This produces an invalid negative grace period.
Use ceiling division without the addition: (nanos - 1) / 1_000_000_000 + 1
Proposed fix
var graceSec int32
if opts.Timeout > 0 {
- // Round up to whole seconds, clamp to MaxInt32 in the unlikely
- // case of a wildly-large timeout.
- secs := (opts.Timeout.Nanoseconds() + 999_999_999) / 1_000_000_000
+ // Round up to whole seconds without overflow, then clamp to MaxInt32.
+ nanos := opts.Timeout.Nanoseconds()
+ secs := (nanos - 1) / 1_000_000_000 + 1
if secs > 1<<31-1 {
secs = 1<<31 - 1
}
graceSec = int32(secs)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var graceSec int32 | |
| if opts.Timeout > 0 { | |
| // Round up to whole seconds, clamp to MaxInt32 in the unlikely | |
| // case of a wildly-large timeout. | |
| secs := (opts.Timeout.Nanoseconds() + 999_999_999) / 1_000_000_000 | |
| if secs > 1<<31-1 { | |
| secs = 1<<31 - 1 | |
| } | |
| graceSec = int32(secs) | |
| var graceSec int32 | |
| if opts.Timeout > 0 { | |
| // Round up to whole seconds without overflow, then clamp to MaxInt32. | |
| nanos := opts.Timeout.Nanoseconds() | |
| secs := (nanos - 1) / 1_000_000_000 + 1 | |
| if secs > 1<<31-1 { | |
| secs = 1<<31 - 1 | |
| } | |
| graceSec = int32(secs) | |
| } |
🤖 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 `@runtime/applecontainer/lifecycle_darwin_arm64.go` around lines 127 - 135, The
timeout rounding currently adds 999_999_999 to opts.Timeout.Nanoseconds(), which
can overflow; update the rounding in the block that sets graceSec (the code that
reads opts.Timeout.Nanoseconds(), computes secs, and assigns graceSec) to use
safe ceiling division without addition: read nanos :=
opts.Timeout.Nanoseconds(), compute secs := (nanos-1)/1_000_000_000 + 1
(ensuring nanos>0), then clamp secs to int64(1<<31-1) and assign graceSec =
int32(secs); adjust the existing secs and clamp logic around that calculation to
prevent negative wraparound.
|
Superseded by #62 (single PR covering the full applecontainer runtime backend). Closing. |
Summary
Fourth PR of M6. Replaces the
RunContainer/StartContainer/StopContainer/RemoveContainerstubs with real implementations.Branched off PR-B (#60) since it depends on the inspect/find helpers
and the JSON envelope contract; rebase onto main after PR-B
merges.
What lands
Bridge (Swift,
lifecycle.swift)ac_run— wrapsContainerClient.createwith a fullContainerConfigurationbuilt from aRunSpecJSON payload.Kernel fetched via
ClientKernel.getDefaultKernel. Image mustalready be in the local content store (pull is PR-F's job).
ac_start—client.bootstrap+process.startin detachedmode (no stdio). Idempotent: a running container is a no-op
success (matches CLI behavior + Docker semantics).
ac_stop— wrapsContainerClient.stopwith a Go-suppliedgrace period (defaults to Apple's 5s SIGTERM + SIGKILL).
ac_delete— wrapsContainerClient.deletewith the force flag.to cover cold first-runs that need to fetch the kernel + init image.
Mount mapping
runtime.MountBind→Filesystem.virtiofs(matches the design§11.1 finding — virtiofs is identity-permissive; this is what we
want for host-directory binds).
runtime.MountTmpfs→Filesystem.tmpfs.runtime.MountVolume→ treated as virtiofs binds for now;real named-volume lifecycle is a later PR.
cgo shim
shim.h/shim.c— four new function-pointer slots + wrappers,resolved via
dlsyminac_load. Partial-failure path resets allpointers and
dlcloses the handle.Runtime methods (
lifecycle_darwin_arm64.go)runSpecToWiremarshalsruntime.RunSpecinto the bridge JSON.Intentionally drops
RunArgs,Privileged,SecurityOpt— theseDocker-specific knobs don't map onto Apple's
containermodel(documented in
design/runtime-applecontainer.md§8). The wiretype doesn't have fields for them, so a future caller depending on
them fails the build instead of silently losing the field.
RunContainerreturns aContainerinStateCreated.StartContaineridempotent via the bridge.StopContainerclampsTimeoutto int32 seconds with overflowguard.
RemoveContainer.RemoveVolumessilently dropped (volume supportdeferred).
mapRunErr/mapLifecycleErrtranslate Apple'snotFounderrors into
runtime.ImageNotFoundError/runtime.ContainerNotFoundError.Inspect path tightened (PR-B file,
inspect_darwin_arm64.go)containerMount.Typenow decodes Apple's enum-with-associated-values shape (
{"virtiofs":{}},{"tmpfs":{}}, …) via a custommountTypeWiredecoder. Was a plain string in PR-B because PR-B'ssmoke tests didn't create any mounts. Surfaces as
runtime.MountBind/MountTmpfs/MountVolume.ReadOnlyderived fromoptionscontaining"ro"(Apple'sactual wire shape, replacing PR-B's synthetic
readonlyfield).Tests
TestLifecycle_EndToEnd— Run → Start → Inspect(running) → Start(idempotent) → Stop → Inspect(stopped) → Remove →
Inspect(notfound). 5s state-poll loop absorbs the apiserver's
async status transitions.
TestRunContainer_MissingImage— assertsImageNotFoundErrorcontract when the caller hasn't pulled theimage first.
TestRunContainer_BindMount— virtiofs round-trip with trailing-slash absorption.
Test plan
make bridge && go test ./runtime/applecontainer/...— fullpackage green
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ./runtime/applecontainer/...— stub still compiles
go vet ./...cleanSummary by CodeRabbit
Release Notes
New Features
Tests