Skip to content

Expose inbound network policy across CLI, SDKs, serve, and NetworkInfo - #1206

Open
G4614 wants to merge 4 commits into
boxlite-ai:mainfrom
G4614:network-info-inbound-outbound-split
Open

Expose inbound network policy across CLI, SDKs, serve, and NetworkInfo#1206
G4614 wants to merge 4 commits into
boxlite-ai:mainfrom
G4614:network-info-inbound-outbound-split

Conversation

@G4614

@G4614 G4614 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #996 (the core NetworkSpec outbound/inbound reshape). This PR is the exposure half: every user-facing surface can now set the inbound policy, and read it back.

  • CLI: --inbound MODE (enabled = public, default; disabled = private). --inbound-allow-net is deliberately not exposed until enforcement exists — a flag that can only error would advertise a feature that doesn't work.
  • serve wire: nested network.inbound accepted alongside outbound.
  • Python/Node/C/Go bindings: inbound setters/fields on the input side, mirroring outbound's shape.
  • Read side: NetworkInfo reshaped to {outbound, inbound, published_ports}, each direction a NetworkDirectionInfo{mode, allow_net}, propagated through all four SDK output bindings (C header regenerated by cbindgen).

A non-empty inbound allowlist stays rejected everywhere (NetworkSpec::try_from, BoxOptions::sanitize, Go buildCOptions) — the field exists for shape symmetry only until a runtime sink enforces it.

Before/after

(after #996, before this PR)
NetworkSpec{outbound, inbound}          — core type exists
  <- but no CLI flag, no SDK setter, no wire field, no read-back:
     inbound is stuck at its default on every surface

(after this PR)
--inbound / JsInboundNetworkSpec / PyInboundNetworkSpec /
boxlite_options_set_network_inbound_{enabled,disabled} / Go InboundNetworkSpec
  -> NetworkSpec.inbound
box.info().network.{outbound,inbound}   — readable back on every surface

Merge-order constraint

This PR makes the REST client send the nested {outbound, inbound} wire shape. The cloud API only understands that shape after #1199 — against the current API, a nested payload 400s (the old DTO's top-level mode is required). #1199 must merge and deploy before this PR (#996 can land at any point in between; its client still sends the legacy flat shape, which every API version accepts).

Stacking note

GitHub can't set a fork branch as base, so this diff shows #996's commits too until #996 merges; review this PR by its head commit (f696d35e5) or wait for #996 to land, after which the diff collapses to just the exposure change (34 files, +1151/−261).

Test plan

  • core 999/999, CLI 194/194, C 72/72 (1 ignored), Node 22/22 — all green
  • Python: cargo check -p boxlite-python --tests clean (cargo test -p boxlite-python fails to link libpython on current main in this environment — pre-existing, verified against a clean origin/main worktree)
  • Go: make test:unit:go green
  • make fmt:check:rust clean

🤖 Generated with Claude Code

Replaces #1198 (closed when its branch was deleted; GitHub can't re-associate a recreated branch).

Summary by CodeRabbit

  • New Features
    • Added separate inbound and outbound network configuration across the CLI, REST API, and SDKs.
    • Added inbound network mode selection, including the ability to disable inbound access.
    • Network status now reports direction-specific modes and allowlists.
  • Bug Fixes
    • Disabled outbound networking no longer creates unnecessary network backends or exposed-port listeners.
  • Documentation
    • Updated quick-start examples and network configuration guidance for the new format.
  • Compatibility
    • Legacy network configuration remains supported where applicable, with validation for conflicting or unsupported settings.

NetworkSpec was a single enum modeling guest egress only; whether a
box's exposed services are publicly reachable had no field anywhere.
Reshape it into a struct with two directions:

Before:
  BoxOptions.network: NetworkSpec::Enabled{allow_net}|Disabled
    <- egress only; no inbound reachability concept
After:
  BoxOptions.network: NetworkSpec{
    outbound: OutboundNetworkSpec::Enabled{allow_net}|Disabled,
    inbound:  InboundNetworkSpec::Enabled{allow_net}|Disabled,
  }

Inbound: Enabled = publicly reachable (default), Disabled = private.
Its allow_net exists for shape symmetry but is rejected when non-empty
(try_from and sanitize) until a runtime sink enforces it. The legacy
flat wire shape still deserializes (untagged fallback) with a
deprecation warning.

CLI/serve/REST client and the C/Node/Python bindings are adapted to
compile against the new shape without exposing inbound configuration;
those surfaces follow in a separate PR. Go is untouched (C ABI
unchanged).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@G4614
G4614 requested a review from a team as a code owner August 12, 2026 06:06
@boxlite-agent

boxlite-agent Bot commented Aug 12, 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":"d0343661-bd47-4303-8109-aa08c2fcb3a7","total_cost_usd":0,"usage":{"output_tokens_details":{"thinking_tokens":0},"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":301,"uuid":"c9b97aec-0105-4e83-87b4-536f7f1e45d1"}

stderr:
<empty>

powered by BoxLite

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 826064fe-4d8b-421f-b605-621ba701c94c

📥 Commits

Reviewing files that changed from the base of the PR and between ef42749 and a4938d0.

📒 Files selected for processing (2)
  • sdks/node/src/options.rs
  • src/cli/src/commands/serve/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • sdks/node/src/options.rs
  • src/cli/src/commands/serve/mod.rs

📝 Walkthrough

Walkthrough

The PR splits network configuration and metadata into independent outbound and inbound policies. It updates runtime networking, CLI and REST handling, C, Go, Node, and Python SDKs, compatibility behavior, validation, serialization, and tests.

Changes

Directional networking

Layer / File(s) Summary
Runtime network model and metadata
src/boxlite/src/runtime/options.rs, src/boxlite/src/runtime/types.rs, src/boxlite/tests/*
The runtime stores separate outbound and inbound policies. It supports nested and legacy forms, validates allowlists, and exposes directional metadata.
Runtime consumers and REST serialization
src/boxlite/src/litebox/..., src/boxlite/src/rest/types.rs
Guest initialization, VMM tasks, and REST box creation use and serialize nested network settings.
CLI network parsing and compatibility
src/cli/src/cli.rs, src/cli/src/commands/serve/*
The CLI adds inbound mode handling, nested request fields, legacy defaults, and mixed-shape validation.
C and Go SDKs
sdks/c/*, sdks/go/*
C and Go APIs expose directional metadata and options. They map both directions and reject unsupported inbound allowlists.
Node SDK
sdks/node/lib/*, sdks/node/src/*, sdks/node/tests/*
The Node SDK validates nested outbound and inbound policies and converts directional metadata.
Python SDK
sdks/python/*
The Python SDK adds nested policy classes, legacy constructor compatibility, public exports, and directional metadata conversion.

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

Possibly related PRs

Suggested reviewers: dorianzheng

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: exposing inbound network policy across the listed user-facing surfaces.
Description check ✅ Passed The description explains the scope, before-and-after behavior, merge dependency, rejected allowlists, and verification results, but omits the exact template section headings.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (2)
sdks/python/src/info.rs (1)

70-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document NetworkDirectionInfo.

NetworkDirectionInfo is a new public Python class. Add a comprehensive class docstring that defines mode and allow_net, including the inbound allowlist limitation.

As per coding guidelines: "Write comprehensive docstrings for all public functions and classes."

🤖 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/python/src/info.rs` around lines 70 - 77, Document the public Python
class represented by PyNetworkDirectionInfo with a comprehensive class
docstring, describing the mode and allow_net attributes and explicitly noting
the inbound allowlist limitation. Keep the existing #[pyclass(name =
"NetworkDirectionInfo")] exposure and field getters unchanged.

Source: Coding guidelines

src/deps/libkrun-sys/vendor/libkrunfw (1)

1-1: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Add CI coverage for the libkrunfw source-build path.

No tracked workflow sets BOXLITE_BUILD_LIBKRUNFW. Add a Linux CI job that sets BOXLITE_BUILD_LIBKRUNFW=1 and runs the relevant build.

🤖 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/deps/libkrun-sys/vendor/libkrunfw` at line 1, Add a Linux CI job in the
existing workflow configuration that sets BOXLITE_BUILD_LIBKRUNFW=1 and executes
the relevant libkrunfw source build, ensuring this source-build path is covered
by CI.
🤖 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/include/boxlite.h`:
- Around line 810-814: Update the AutoStop documentation for
boxlite_options_set_auto_stop_interval in sdks/c/include/boxlite.h lines 810-814
and its corresponding description in sdks/c/src/options.rs lines 169-172,
replacing “remain paused” with “remain idle” while preserving the rest of the
behavior and wording.

In `@sdks/go/options.go`:
- Around line 82-90: Update the InboundNetworkSpec comment to state that
AllowNet is reserved and must remain empty until inbound allowlist enforcement
is implemented; remove the claim that it currently restricts reachable
hosts/IPs, while preserving the existing ModeEnabled and ModeDisabled
descriptions.

In `@sdks/node/lib/simplebox.ts`:
- Around line 149-154: Update the directional policy validation in SimpleBox
construction to reject record-valued outbound or inbound policies that do not
define mode, while preserving the existing object-type checks. Add regression
tests in sdks/node/tests/options.test.ts:220-228 covering empty outbound and
inbound policy objects; both sites require changes.

In `@sdks/node/src/options.rs`:
- Around line 369-381: Update the inbound network documentation and conversion
behavior: in sdks/node/src/options.rs:369-381,
sdks/node/lib/native-contracts.ts:118-122, and
sdks/node/lib/simplebox.ts:117-122, state that non-empty inbound allowNet values
are currently rejected and cannot restrict access; in
sdks/node/src/options.rs:993-1008, extend the conversion test to assert that a
non-empty inbound allowlist returns an error.

In `@sdks/python/src/options.rs`:
- Around line 276-281: Update the inbound network policy documentation near
InboundNetworkSpec to state that non-empty allow_net values are currently
invalid and rejected because inbound allowlist enforcement is unavailable;
remove the claim that allow_net restricts inbound access, while preserving the
existing mode descriptions.

In `@sdks/python/tests/test_network_spec.py`:
- Around line 17-44: Extend NetworkSpec tests in
sdks/python/tests/test_network_spec.py (lines 17-44) with nested
InboundNetworkSpec coverage, including mode="disabled" and assertions for the
resulting inbound policy. Add a conversion test in sdks/python/src/options.rs
(lines 1091-1101) that uses a non-empty inbound allow_net and asserts conversion
fails.

In `@src/boxlite/src/runtime/options.rs`:
- Around line 860-910: Update PortPublishTask’s fresh-publication and
reattach-reconciliation paths to inspect network.inbound and skip backend.expose
when it is InboundNetworkSpec::Disabled, while preserving existing publication
for Enabled. Add an integration test using a requested PortSpec that verifies no
listener is created for an inbound-disabled box.

In `@src/cli/src/cli.rs`:
- Around line 651-655: Update the help text for the inbound field in the CLI
argument definition to explicitly state that --network disabled does not disable
inbound access; callers must pass --inbound disabled to make services private
and unreachable externally. Preserve the existing enabled/disabled mode
descriptions.

In `@src/cli/src/commands/serve/types.rs`:
- Around line 112-120: Update the documentation above InboundNetworkSpec to
state that inbound allow_net must remain empty and non-empty values are rejected
until enforcement is implemented; remove the claim that it restricts publicly
reachable services, while preserving the mode descriptions.

---

Nitpick comments:
In `@sdks/python/src/info.rs`:
- Around line 70-77: Document the public Python class represented by
PyNetworkDirectionInfo with a comprehensive class docstring, describing the mode
and allow_net attributes and explicitly noting the inbound allowlist limitation.
Keep the existing #[pyclass(name = "NetworkDirectionInfo")] exposure and field
getters unchanged.

In `@src/deps/libkrun-sys/vendor/libkrunfw`:
- Line 1: Add a Linux CI job in the existing workflow configuration that sets
BOXLITE_BUILD_LIBKRUNFW=1 and executes the relevant libkrunfw source build,
ensuring this source-build path is covered by CI.
🪄 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: 1ae6d7c9-c9ea-4f74-b9a2-8a1923b23603

📥 Commits

Reviewing files that changed from the base of the PR and between 038938c and f696d35.

📒 Files selected for processing (40)
  • sdks/c/README.md
  • sdks/c/include/boxlite.h
  • sdks/c/src/event_queue.rs
  • sdks/c/src/info.rs
  • sdks/c/src/options.rs
  • sdks/go/boxlite_test.go
  • sdks/go/info.go
  • sdks/go/info_cgo_dev_test.go
  • sdks/go/info_cgo_test_support_dev.go
  • sdks/go/network_secrets_integration_test.go
  • sdks/go/options.go
  • sdks/node/README.md
  • sdks/node/lib/native-contracts.ts
  • sdks/node/lib/simplebox.ts
  • sdks/node/src/info.rs
  • sdks/node/src/options.rs
  • sdks/node/tests/network-secrets.integration.test.ts
  • sdks/node/tests/options.test.ts
  • sdks/node/tests/skillbox.integration.test.ts
  • sdks/python/README.md
  • sdks/python/boxlite/__init__.py
  • sdks/python/src/info.rs
  • sdks/python/src/lib.rs
  • sdks/python/src/options.rs
  • sdks/python/tests/test_network_spec.py
  • sdks/python/tests/test_secret_substitution.py
  • sdks/python/tests/test_tcp_filter.py
  • src/boxlite/src/lib.rs
  • src/boxlite/src/litebox/init/tasks/guest_init.rs
  • src/boxlite/src/litebox/init/tasks/vmm_attach.rs
  • src/boxlite/src/litebox/init/tasks/vmm_spawn.rs
  • src/boxlite/src/rest/types.rs
  • src/boxlite/src/runtime/options.rs
  • src/boxlite/src/runtime/types.rs
  • src/boxlite/tests/network_spec.rs
  • src/boxlite/tests/security_enforcement.rs
  • src/cli/src/cli.rs
  • src/cli/src/commands/serve/mod.rs
  • src/cli/src/commands/serve/types.rs
  • src/deps/libkrun-sys/vendor/libkrunfw

Comment thread sdks/c/include/boxlite.h
Comment on lines +810 to 814
// Set how long an idle box may remain paused before the runtime pauses it.
//
// The value is expressed in seconds. `0` preserves the runtime/control-plane
// default. A null options pointer is treated as a no-op.
void boxlite_options_set_auto_stop_interval(CBoxliteOptions *opts, uint32_t seconds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the AutoStop state description.

An idle box is not already paused before AutoStop pauses it. Replace “remain paused” with “remain idle” in both API comments.

  • sdks/c/include/boxlite.h#L810-L814: describe the idle duration before AutoStop pauses the box.
  • sdks/c/src/options.rs#L169-L172: use the same corrected AutoStop description.
📍 Affects 2 files
  • sdks/c/include/boxlite.h#L810-L814 (this comment)
  • sdks/c/src/options.rs#L169-L172
🤖 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/include/boxlite.h` around lines 810 - 814, Update the AutoStop
documentation for boxlite_options_set_auto_stop_interval in
sdks/c/include/boxlite.h lines 810-814 and its corresponding description in
sdks/c/src/options.rs lines 169-172, replacing “remain paused” with “remain
idle” while preserving the rest of the behavior and wording.

Comment thread sdks/go/options.go
Comment on lines +149 to +154
if ("outbound" in value && !isRecord(value.outbound)) {
throw new TypeError("SimpleBoxOptions.network.outbound must be an object.");
}
if ("inbound" in value && !isRecord(value.inbound)) {
throw new TypeError("SimpleBoxOptions.network.inbound must be an object.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject incomplete directional policies in the constructor.

isRecord({}) passes Lines 149-154, so { network: { outbound: {} } } reaches lazy native creation instead of failing in SimpleBox construction. Require mode for each supplied directional policy.

  • sdks/node/lib/simplebox.ts#L149-L154: reject record-valued outbound or inbound policies that omit mode.
  • sdks/node/tests/options.test.ts#L220-L228: add regression cases for empty outbound and inbound policy objects.

As per coding guidelines, sdks/node/**/*.test.{js,ts,jsx,tsx} must “Write unit tests for all public functions and critical business logic.”

📍 Affects 2 files
  • sdks/node/lib/simplebox.ts#L149-L154 (this comment)
  • sdks/node/tests/options.test.ts#L220-L228
🤖 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/node/lib/simplebox.ts` around lines 149 - 154, Update the directional
policy validation in SimpleBox construction to reject record-valued outbound or
inbound policies that do not define mode, while preserving the existing
object-type checks. Add regression tests in
sdks/node/tests/options.test.ts:220-228 covering empty outbound and inbound
policy objects; both sites require changes.

Source: Coding guidelines

Comment thread sdks/node/src/options.rs
Comment on lines +369 to +381
/// Aligned field-for-field with `JsOutboundNetworkSpec`: `mode="enabled"`
/// means services the box exposes are publicly reachable (optionally
/// restricted to `allowNet`); `mode="disabled"` means private.
#[napi(object)]
#[derive(Clone, Debug)]
pub struct JsInboundNetworkSpec {
/// Inbound mode: "enabled" or "disabled".
pub mode: String,

/// Inbound allowlist when mode is "enabled". Empty/omitted means any
/// caller may reach the box's exposed services.
#[napi(js_name = "allowNet")]
pub allow_net: Option<Vec<String>>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe inbound.allowNet as enforceable.

The runtime rejects non-empty inbound allowlists because no layer enforces them. The current public comments state that callers can restrict inbound access with allowNet. This gives users an unsupported security configuration.

  • sdks/node/src/options.rs#L369-L381: state that non-empty inbound allowNet is currently rejected.
  • sdks/node/lib/native-contracts.ts#L118-L122: document the same restriction in the native TypeScript contract.
  • sdks/node/lib/simplebox.ts#L117-L122: document the same restriction in the high-level SDK contract.
  • sdks/node/src/options.rs#L993-L1008: add a conversion test that asserts a non-empty inbound allowlist returns an error.

As per coding guidelines, sdks/node/**/*.test.{js,ts,jsx,tsx} must “Write unit tests for all public functions and critical business logic.”

📍 Affects 3 files
  • sdks/node/src/options.rs#L369-L381 (this comment)
  • sdks/node/lib/native-contracts.ts#L118-L122
  • sdks/node/lib/simplebox.ts#L117-L122
  • sdks/node/src/options.rs#L993-L1008
🤖 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/node/src/options.rs` around lines 369 - 381, Update the inbound network
documentation and conversion behavior: in sdks/node/src/options.rs:369-381,
sdks/node/lib/native-contracts.ts:118-122, and
sdks/node/lib/simplebox.ts:117-122, state that non-empty inbound allowNet values
are currently rejected and cannot restrict access; in
sdks/node/src/options.rs:993-1008, extend the conversion test to assert that a
non-empty inbound allowlist returns an error.

Source: Coding guidelines

Comment on lines +276 to +281
/// Inbound network policy.
///
/// Aligned field-for-field with `OutboundNetworkSpec`: `mode` accepts
/// `"enabled"` (services the box exposes are publicly reachable) or
/// `"disabled"` (private). `allow_net` restricts which hosts/IPs may reach
/// in when `mode="enabled"`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Correct the inbound allowlist documentation.

The runtime rejects non-empty inbound allow_net values because no layer enforces them. These lines state that allow_net restricts inbound access. This implies an access-control guarantee that does not exist. State that non-empty inbound allowlists are currently invalid and rejected.

The PR objective states that inbound allowlist enforcement is not available.

🤖 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/python/src/options.rs` around lines 276 - 281, Update the inbound
network policy documentation near InboundNetworkSpec to state that non-empty
allow_net values are currently invalid and rejected because inbound allowlist
enforcement is unavailable; remove the claim that allow_net restricts inbound
access, while preserving the existing mode descriptions.

Comment on lines +17 to +44
"""NetworkSpec accepts the nested outbound shape."""
spec = boxlite.NetworkSpec(
outbound=boxlite.OutboundNetworkSpec(
mode="enabled",
allow_net=["example.com", "*.openai.com"],
)
)

assert spec.outbound.mode == "enabled"
assert spec.outbound.allow_net == ["example.com", "*.openai.com"]

def test_legacy_creation(self):
"""NetworkSpec keeps accepting the legacy mode and allow_net keywords."""
spec = boxlite.NetworkSpec(
mode="enabled",
allow_net=["example.com", "*.openai.com"],
)

assert spec.mode == "enabled"
assert spec.allow_net == ["example.com", "*.openai.com"]
assert spec.outbound.mode == "enabled"
assert spec.outbound.allow_net == ["example.com", "*.openai.com"]

def test_rejects_mixed_legacy_and_nested_outbound(self):
"""NetworkSpec rejects callers that mix nested and legacy outbound fields."""
with pytest.raises(ValueError):
boxlite.NetworkSpec(
outbound=boxlite.OutboundNetworkSpec(mode="enabled"),
mode="disabled",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add regression coverage for inbound policy validation.

The new inbound API has no test for the unsupported inbound allowlist path. A regression could accept an inbound allowlist even though runtime enforcement is unavailable.

  • sdks/python/tests/test_network_spec.py#L17-L44: construct and assert a nested InboundNetworkSpec, including mode="disabled".
  • sdks/python/src/options.rs#L1091-L1101: convert an inbound policy with a non-empty allow_net and assert that conversion fails.

Based on learnings: "Write unit tests for critical functionality and edge cases."

📍 Affects 2 files
  • sdks/python/tests/test_network_spec.py#L17-L44 (this comment)
  • sdks/python/src/options.rs#L1091-L1101
🤖 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/python/tests/test_network_spec.py` around lines 17 - 44, Extend
NetworkSpec tests in sdks/python/tests/test_network_spec.py (lines 17-44) with
nested InboundNetworkSpec coverage, including mode="disabled" and assertions for
the resulting inbound policy. Add a conversion test in
sdks/python/src/options.rs (lines 1091-1101) that uses a non-empty inbound
allow_net and asserts conversion fails.

Source: Learnings

Comment on lines +860 to +910
/// Whether services the box exposes are reachable from outside it. Mirrors
/// [`OutboundNetworkSpec`]'s shape: `Enabled` = publicly reachable,
/// `Disabled` = private, unreachable from outside the box. `allow_net`
/// exists for shape symmetry but must be empty today — a non-empty inbound
/// allowlist is rejected (`try_from`/`sanitize`) until enforcement exists.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum InboundNetworkSpec {
Enabled {
#[serde(default)]
allow_net: Vec<String>,
},
Disabled,
}

impl Default for OutboundNetworkSpec {
fn default() -> Self {
Self::Enabled {
allow_net: Vec::new(),
}
}
}

impl Default for InboundNetworkSpec {
// Public unless told otherwise, matching the control plane's
// longstanding preview-URL default.
fn default() -> Self {
Self::Enabled {
allow_net: Vec::new(),
}
}
}

impl NetworkSpec {
pub fn enabled(allow_net: Vec<String>) -> Self {
Self {
outbound: OutboundNetworkSpec::Enabled { allow_net },
inbound: InboundNetworkSpec::default(),
}
}

pub fn disabled() -> Self {
Self {
outbound: OutboundNetworkSpec::Disabled,
inbound: InboundNetworkSpec::default(),
}
}

pub fn with_inbound(mut self, inbound: InboundNetworkSpec) -> Self {
self.inbound = inbound;
self
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/boxlite/src/litebox --items all --type function --match 'Port|Publish|Network'
rg -n -C 4 'network\.inbound|InboundNetworkSpec|PortPublish|published_ports|publish' \
  src/boxlite/src/litebox src/boxlite/tests

Repository: boxlite-ai/boxlite

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- port publication structure ---'
ast-grep outline src/boxlite/src/litebox/init/tasks/port_publish.rs --items all --type function
echo '--- port publication implementation ---'
sed -n '1,280p' src/boxlite/src/litebox/init/tasks/port_publish.rs
echo '--- task entry points and config flow ---'
rg -n -C 8 'PortPublishTask|PortPublisher::|planned|options\.ports|build_network_backend|NetworkBackendConfig|InboundNetworkSpec|network\.inbound' \
  src/boxlite/src/litebox/init/tasks/port_publish.rs \
  src/boxlite/src/litebox/init/mod.rs \
  src/boxlite/src/litebox/init/tasks/vmm_spawn.rs \
  src/boxlite/src/litebox/init/tasks/vmm_attach.rs \
  src/boxlite/src/litebox/config.rs \
  src/boxlite/src/runtime/options.rs

Repository: boxlite-ai/boxlite

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

publication = Path("src/boxlite/src/litebox/init/tasks/port_publish.rs").read_text()
spawn = Path("src/boxlite/src/litebox/init/tasks/vmm_spawn.rs").read_text()
tests = "\n".join(
    p.read_text()
    for root in ("src/boxlite/tests", "src/boxlite/src")
    for p in Path(root).rglob("*.rs")
)

checks = {
    "publication_reads_ports": "ctx.config.options.ports.clone()" in publication,
    "publication_reads_inbound": "network.inbound" in publication or "options.network.inbound" in publication,
    "publication_calls_expose": ".backend\n            .expose(" in publication,
    "backend_branches_on_outbound": "match &options.network.outbound" in spawn,
    "backend_reads_inbound": "options.network.inbound" in spawn,
    "disabled_inbound_test_present": "InboundNetworkSpec::Disabled" in tests
        and ("port" in tests.lower() or "publish" in tests.lower()),
}

for name, value in checks.items():
    print(f"{name}={value}")

assert checks["publication_reads_ports"]
assert not checks["publication_reads_inbound"]
assert checks["publication_calls_expose"]
assert checks["backend_branches_on_outbound"]
assert not checks["backend_reads_inbound"]
assert not checks["disabled_inbound_test_present"]
PY

Repository: boxlite-ai/boxlite

Length of output: 433


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- inbound policy references ---'
rg -n -C 6 'InboundNetworkSpec|inbound' src/boxlite/src src/boxlite/tests

echo '--- publication tests involving disabled policies or requested ports ---'
rg -n -C 10 'Disabled|disabled|mapping\(|PortSpec|publish\(|reconcile\(' \
  src/boxlite/src/litebox/init/tasks/port_publish.rs \
  src/boxlite/tests

Repository: boxlite-ai/boxlite

Length of output: 50375


Enforce InboundNetworkSpec::Disabled during port publication

PortPublishTask reads options.ports and calls backend.expose without reading network.inbound. With outbound networking enabled, inbound-disabled boxes can still publish requested ports. Guard fresh publication and reattach reconciliation, then add an integration test with a requested PortSpec that asserts no listener is created.

🤖 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/options.rs` around lines 860 - 910, Update
PortPublishTask’s fresh-publication and reattach-reconciliation paths to inspect
network.inbound and skip backend.expose when it is InboundNetworkSpec::Disabled,
while preserving existing publication for Enabled. Add an integration test using
a requested PortSpec that verifies no listener is created for an
inbound-disabled box.

Comment thread src/cli/src/cli.rs
Comment thread src/cli/src/commands/serve/types.rs Outdated
Comment on lines 112 to 120
/// Aligned field-for-field with [`OutboundNetworkSpec`]: `mode="enabled"`
/// means services the box exposes are publicly reachable (optionally
/// restricted to `allow_net`); `mode="disabled"` means private.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct InboundNetworkSpec {
pub mode: String,
#[serde(default)]
pub allow_net: Vec<String>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not document inbound allowlists as enforced.

The runtime rejects non-empty inbound allow_net values because enforcement is not available. Lines 112-114 state that allow_net restricts public access. Clients can rely on this text and then receive a configuration error. State that inbound allow_net must remain empty until enforcement is implemented.

Proposed documentation correction
-/// restricted to `allow_net`); `mode="disabled"` means private.
+/// `allow_net` must be empty until inbound allowlist enforcement is available.
+/// `mode="disabled"` means private.

The PR objective states that non-empty inbound allowlists remain rejected.

🤖 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/cli/src/commands/serve/types.rs` around lines 112 - 120, Update the
documentation above InboundNetworkSpec to state that inbound allow_net must
remain empty and non-empty values are rejected until enforcement is implemented;
remove the claim that it restricts publicly reachable services, while preserving
the mode descriptions.

@G4614
G4614 force-pushed the network-info-inbound-outbound-split branch 2 times, most recently from 236c112 to ef42749 Compare August 12, 2026 10:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/cli/src/cli.rs (1)

1451-1462: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the default inbound policy.

This test states that a bare command preserves the default network policy, but it only asserts outbound behavior. Add an assertion that opts.network.inbound is InboundNetworkSpec::Enabled with an empty allowlist. This pins the required public inbound default and prevents a compatibility regression.

Proposed test update
         assert!(
             matches!(opts.network.outbound, OutboundNetworkSpec::Enabled { ref allow_net } if allow_net.is_empty())
         );
+        assert!(
+            matches!(opts.network.inbound, InboundNetworkSpec::Enabled { ref allow_net } if allow_net.is_empty())
+        );
🤖 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/cli/src/cli.rs` around lines 1451 - 1462, Extend
test_network_flags_default_left_untouched to also assert that
opts.network.inbound matches InboundNetworkSpec::Enabled with an empty
allowlist, preserving the existing outbound assertion and confirming the default
inbound policy for a bare run.
🤖 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.

Nitpick comments:
In `@src/cli/src/cli.rs`:
- Around line 1451-1462: Extend test_network_flags_default_left_untouched to
also assert that opts.network.inbound matches InboundNetworkSpec::Enabled with
an empty allowlist, preserving the existing outbound assertion and confirming
the default inbound policy for a bare run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cf362336-4763-41cb-be9a-27e8ddc8d6f4

📥 Commits

Reviewing files that changed from the base of the PR and between 236c112 and ef42749.

📒 Files selected for processing (5)
  • sdks/go/boxlite_test.go
  • sdks/go/options.go
  • sdks/node/src/options.rs
  • src/boxlite/src/runtime/options.rs
  • src/cli/src/cli.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • sdks/go/options.go
  • sdks/node/src/options.rs
  • sdks/go/boxlite_test.go
  • src/boxlite/src/runtime/options.rs

NetworkSpec::enabled/disabled read as whole-spec constructors but only
set the outbound direction, leaving inbound at its default — invisible
at the call site now that inbound exists. Rename to
outbound_enabled/outbound_disabled and document what each leaves
untouched.

Same problem in the messages: errors saying "network.mode" point
nested callers at a field that no longer exists. Say
network.outbound.mode where the check is outbound-specific, and make
NetworkMode::from_str's message direction-neutral — it parses both
directions now, so "invalid network.mode" was wrong for inbound input.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@G4614
G4614 force-pushed the network-info-inbound-outbound-split branch from ef42749 to a4938d0 Compare August 12, 2026 11:53
G4614 and others added 2 commits August 12, 2026 20:27
Turning NetworkSpec into a two-direction struct broke every pre-split
Rust caller: brace-variant literals and match arms have no struct
equivalent, so there was no migration short of editing each site.

Give the name back to the outbound enum instead — same name, same
variants, same shape — and call the container NetworkPolicy. Pre-split
literals and match arms now compile untouched; assigning one to
BoxOptions::network needs only .into(), via a new
From<NetworkSpec> for NetworkPolicy.

OutboundNetworkSpec survives as a direction-explicit alias for new code,
since the bare name reads as if it covered both directions.

network_spec.rs::pre_split_network_spec_source_shape_still_compiles
pins the contract: it exercises the old literal, match, and assignment
forms, so a future reshape that breaks them fails the build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Builds on the NetworkSpec outbound/inbound split (the core reshape in
this PR's base): every user-facing surface can now set the inbound
policy, and read it back.

- CLI: --inbound MODE (enabled=public default, disabled=private);
  --inbound-allow-net is deliberately not exposed until enforcement
  exists
- serve wire: nested network.inbound accepted alongside outbound
- Python/Node/C/Go bindings: inbound setters/fields on the input side,
  mirroring outbound's shape
- NetworkInfo reshaped to {outbound, inbound, published_ports}, each
  direction a NetworkDirectionInfo{mode, allow_net}, propagated through
  all four SDK output bindings (C header regenerated by cbindgen)

A non-empty inbound allowlist stays rejected everywhere (try_from,
sanitize, Go buildCOptions) — the field exists for shape symmetry only
until a runtime sink enforces it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@G4614
G4614 force-pushed the network-info-inbound-outbound-split branch from a4938d0 to 532ca4b Compare August 12, 2026 13:03
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.

1 participant