Skip to content

fix(api): default box visibility to private - #1370

Merged
DefeatMan merged 3 commits into
boxlite-ai:mainfrom
G4614:pol-205-anonymous-public-warning
Aug 28, 2026
Merged

DefeatMan merged 3 commits into
boxlite-ai:mainfrom
G4614:pol-205-anonymous-public-warning

Conversation

@G4614

@G4614 G4614 commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes POL-205.

Before

POST /v1/boxes {}  (no network.inbound specified)
  -> createBoxToCreateBox(dto)          [box-to-box.mapper.ts]
       createDto.public = undefined     (inbound.mode not specified)
  -> BoxService.create()                [box.service.ts:291]
       box.public = createBoxDto.public ?? true   ← BUG: silently public
  -> anonymous internet traffic reaches the box's preview URL, no auth

Same bug in assignWarmPoolBox (box.service.ts:355) for boxes served from
the warm pool.

After

POST /v1/boxes {}
  -> createBoxToCreateBox(dto) -> createDto.public = undefined
  -> BoxService.create()
       box.public = createBoxDto.public ?? false   (both call sites)
  -> box is private by default; caller must opt in with
     network.inbound.mode="enabled" (or legacy public:true) to be reachable

This supersedes the Warning-header mitigation from the branch's prior commit —
once the default is private, a caller can no longer end up anonymously public
without asking, so the header (and its isAnonymouslyPublicByDefault helper)
served no further purpose and were removed.

Note: this is a breaking behavior change for any existing caller relying on
the old anonymous-public default. Flagging for release notes / staged rollout
per the issue's own migration plan.

Test plan

  • box.service.spec.ts's existing public defaults tests pin both call
    sites; updated their expected values for the new default (undefined -> false,
    explicit true -> true) — 682 passed, 72 skipped, 0 failed on the full api
    suite
  • Two-side verification: reverted the two ?? false lines back to ?? true,
    confirmed both pinning tests fail with the expected "expect false, got true"
    diff; restored, all green again
  • tsc --noEmit clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Newly created boxes now default to private when visibility is not specified.
    • Warm-pool assigned boxes also default to private.
    • Explicitly requested visibility settings continue to be honored.
  • Bug Fixes

    • Tunnel requests to private boxes are now rejected.
    • Removed the warning response previously returned for boxes without an inbound network mode.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

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: 4813b9e0-49ca-4ee8-a29c-e28fd5ed4d97

📥 Commits

Reviewing files that changed from the base of the PR and between 95148a5 and 2ff2a4a.

📒 Files selected for processing (2)
  • apps/api/src/boxlite-rest/boxlite-proxy.controller.spec.ts
  • apps/api/src/boxlite-rest/boxlite-proxy.controller.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

Box creation defaults fresh and warm-pool boxes to private visibility when unspecified. The create-box controller no longer sets anonymous-public warning headers. Tunnel requests for private boxes now return a conflict before URL resolution.

Changes

Private visibility defaults

Layer / File(s) Summary
Apply private visibility defaults
apps/api/src/box/services/box.service.ts, apps/api/src/box/services/box.service.spec.ts
Fresh and warm-pool boxes default public to false. Tests verify unspecified values and explicit true values.
Remove anonymous-public warning response
apps/api/src/boxlite-rest/boxlite-box.controller.ts
createBox no longer receives an Express response or sets the Warning header for implicit public access.
Guard private tunnel access
apps/api/src/boxlite-rest/boxlite-proxy.controller.ts, apps/api/src/boxlite-rest/boxlite-proxy.controller.spec.ts
proxyNetworkTunnel rejects private boxes with a 409 status before calling getNetworkTunnelUrl. Tests and mock data cover the public visibility field.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: 🟡 Moderate · up to 2ff2a

The PR changes omitted visibility to private and adds checks before issuing tunnel URLs, but a recently public box can remain anonymously reachable for up to three seconds after being changed to private because downstream public-status caching is not invalidated. This bounded security window should be fixed or explicitly accepted before merge.

Suggested reviewers: defeatman

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: box visibility now defaults to private.
Description check ✅ Passed The description provides the issue reference, before-and-after call paths, change rationale, test coverage, verification steps, and breaking-change risk. It omits some template headings, but the requi…
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.
Full details: Description check

Explanation

The description provides the issue reference, before-and-after call paths, change rationale, test coverage, verification steps, and breaking-change risk. It omits some template headings, but the required information is mostly present.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 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.

@G4614
G4614 force-pushed the pol-205-anonymous-public-warning branch from 270a956 to 3a66143 Compare August 28, 2026 07:39
@G4614
G4614 marked this pull request as ready for review August 28, 2026 08:05
@G4614
G4614 requested a review from a team as a code owner August 28, 2026 08:05
@boxlite-agent

boxlite-agent Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

📦 BoxLite review — couldn't complete

claude exited 1

stdout:
{"duration_api_ms":0,"stop_reason":"stop_sequence","session_id":"a4fd7635-94a0-4cd3-b9b2-e8b464bb7d4a","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","subagent_stats":{"spawned":0,"requested":{"background":0,"foreground":0,"unset":0},"started_in_background":0,"max_depth":0,"spawned_by_subagents":0,"completed":0,"failed":0,"killed":{"parent":0,"user":0,"system":0},"refused":{"depth_limit":0,"concurrency_limit":0,"budget":0},"by_type":{}},"is_error":true,"num_turns":1,"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":336,"uuid":"99c2f399-0d8e-406b-a675-ad42b0db9d78","queued_turn_count":0}

stderr:
<empty>

powered by BoxLite

@G4614 G4614 changed the title fix(api): warn when a box falls through to the anonymous-public default fix(api): default box visibility to private Aug 28, 2026
G4614 added 2 commits August 28, 2026 21:22
Phase 1 of POL-205. A caller who never mentions network.inbound at
create time gets an anonymously-public box via the `?? true` fallback
in box.service.ts, with no signal that this happened — the API just
returns 201.

Add isAnonymouslyPublicByDefault() (box-to-box.mapper.ts) to detect
exactly that case — the request had no explicit inbound.mode, not an
opt-in — and set a Warning response header on box creation so the
gap is visible before the default itself changes.

Does not touch the default value or the three-state inbound.mode
design (token/public/disabled) from POL-205's Phase 2 — this is only
the visibility half, per the issue's own migration plan.
Fixes POL-205. Boxes were reachable from the public internet by
default: an unspecified network.inbound fell through to
`createBoxDto.public ?? true` in both the fresh-box and
warm-pool-assignment paths, with no way to discover this short
of probing the box's own tunnel URL.

Flips both defaults to false. Supersedes the Warning-header
mitigation from the prior commit on this branch — once the
default is private, a caller can no longer end up anonymously
public without asking, so the header served no further purpose.
@G4614
G4614 force-pushed the pol-205-anonymous-public-warning branch from 95148a5 to 64d3878 Compare August 28, 2026 12:22
Private boxes (the new default) reject tunnel URL requests with 409.
Callers must explicitly set public: true before a tunnel URL is issued.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@DefeatMan DefeatMan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@DefeatMan
DefeatMan merged commit 446e9ae into boxlite-ai:main Aug 28, 2026
27 checks passed
DefeatMan added a commit to DefeatMan/boxlite that referenced this pull request Sep 22, 2026
Running this suite against api.dev.boxlite.ai found three defects in
what this branch had just added, and three areas the cloud path never
covered.

The sweep selected by idle time alone: a report against dev listed five
of a colleague's POL-599 boxes as candidates. Boxes the cases create are
now named `e2e-<random>` and only that prefix is swept; `--any-name`
is the opt-out and an empty prefix is refused. The sweep also waits
for a box to stop being served, and only a 404 says it is gone.

Completing one lifecycle window beside a caller's other one could build
a pair the SDK rejects, so the SDK door applies both or neither. A
hand-built REST body meets no such rule and is completed and named. The
workflow now refuses a stage outside dev/prod and a non-https API_URL.
The Node tunnel driver only stopped its four boxes; it removes them now.

Ported from the local suite, none of it reachable there before: the
box's main command, secrets, and a read-only mount refusal. Tunnel and
preview become xfail(strict) — since boxlite-ai#1370 no SDK caller can create a
public box, and raw REST with the nested shape shows that is the
client's fault, not the server's.

test_harness_guards.py covers the suite's own rules without a stage,
each one run first against the defect it reproduces. What is not
verified is recorded in apps/e2e/README.md: the volume cases skip on
dev, whose key holds no volume permission, and short-exec stdout is
dropped there until boxlite-ai#1569 lands.
DefeatMan added a commit to DefeatMan/boxlite that referenced this pull request Sep 23, 2026
Every CLI case in the suite detaches, so `boxlite run <image> <cmd>`
without `-d` was never exercised — and on a cloud stage it fails every
time. That form is a different path, not a nicer spelling of the same
one: create → WS `/boxes/{id}/attach` → `POST /start`, and that
upgrade comes back a bare 503 whose body (`upstream connect error or
disconnect/reset before headers`) is an envelope the API never writes.
So the refusal comes from the load balancing in front of it; which hop
drops the upgrade is not established. A green suite said nothing about
any of it.

The case is `xfail(strict=True)`, matching how the tunnel cases carry
boxlite-ai#1370, but conditional on a remote stage host where those are
unconditional. The cause is stage topology, not product code: a local
stack reaches boxlite-api on :3000 with no load balancer in front of
it, so an unconditional mark would turn a local pass into an XPASS
failure and take `make test:e2e` red. What a local stack does with the
attached form is untested, and the mark claims nothing about it.

`--runxfail` confirms the failure signal against dev is the 503 itself,
not a parse or setup error.

The create lands before the 503, so a failed run still costs a box and
never prints its id. The case therefore names its box up front and
removes it by name in `finally`; three runs against dev left nothing
behind. `--rm` cannot do that job — it conflicts with the lifecycle
deadlines, which are what reclaims the box if the removal itself
fails. That opt-in is also the one exception to conftest's note that
CLI boxes are beyond sweep.py's reach, so the note now says so.
DefeatMan added a commit to DefeatMan/boxlite that referenced this pull request Sep 24, 2026
Running this suite against api.dev.boxlite.ai found three defects in
what this branch had just added, and three areas the cloud path never
covered.

The sweep selected by idle time alone: a report against dev listed five
of a colleague's POL-599 boxes as candidates. Boxes the cases create are
now named `e2e-<random>` and only that prefix is swept; `--any-name`
is the opt-out and an empty prefix is refused. The sweep also waits
for a box to stop being served, and only a 404 says it is gone.

Completing one lifecycle window beside a caller's other one could build
a pair the SDK rejects, so the SDK door applies both or neither. A
hand-built REST body meets no such rule and is completed and named. The
workflow now refuses a stage outside dev/prod and a non-https API_URL.
The Node tunnel driver only stopped its four boxes; it removes them now.

Ported from the local suite, none of it reachable there before: the
box's main command, secrets, and a read-only mount refusal. Tunnel and
preview become xfail(strict) — since boxlite-ai#1370 no SDK caller can create a
public box, and raw REST with the nested shape shows that is the
client's fault, not the server's.

test_harness_guards.py covers the suite's own rules without a stage,
each one run first against the defect it reproduces. What is not
verified is recorded in apps/e2e/README.md: the volume cases skip on
dev, whose key holds no volume permission, and short-exec stdout is
dropped there until boxlite-ai#1569 lands.
DefeatMan added a commit to DefeatMan/boxlite that referenced this pull request Sep 24, 2026
Every CLI case in the suite detaches, so `boxlite run <image> <cmd>`
without `-d` was never exercised — and on a cloud stage it fails every
time. That form is a different path, not a nicer spelling of the same
one: create → WS `/boxes/{id}/attach` → `POST /start`, and that
upgrade comes back a bare 503 whose body (`upstream connect error or
disconnect/reset before headers`) is an envelope the API never writes.
So the refusal comes from the load balancing in front of it; which hop
drops the upgrade is not established. A green suite said nothing about
any of it.

The case is `xfail(strict=True)`, matching how the tunnel cases carry
boxlite-ai#1370, but conditional on a remote stage host where those are
unconditional. The cause is stage topology, not product code: a local
stack reaches boxlite-api on :3000 with no load balancer in front of
it, so an unconditional mark would turn a local pass into an XPASS
failure and take `make test:e2e` red. What a local stack does with the
attached form is untested, and the mark claims nothing about it.

`--runxfail` confirms the failure signal against dev is the 503 itself,
not a parse or setup error.

The create lands before the 503, so a failed run still costs a box and
never prints its id. The case therefore names its box up front and
removes it by name in `finally`; three runs against dev left nothing
behind. `--rm` cannot do that job — it conflicts with the lifecycle
deadlines, which are what reclaims the box if the removal itself
fails. That opt-in is also the one exception to conftest's note that
CLI boxes are beyond sweep.py's reach, so the note now says so.
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.

2 participants