Skip to content

refactor(volume): simplify state, rename source to volume_id, allow name on REST create - #1208

Open
G4614 wants to merge 1 commit into
boxlite-ai:mainfrom
G4614:feat/volume-api-cleanup
Open

refactor(volume): simplify state, rename source to volume_id, allow name on REST create#1208
G4614 wants to merge 1 commit into
boxlite-ai:mainfrom
G4614:feat/volume-api-cleanup

Conversation

@G4614

@G4614 G4614 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Three related cleanups to the managed-volume feature landed by #1191/#1192/#1056.

1. VolumeState simplification — drops pending_create/pending_delete (the reconciler's per-volume Redis lock already distinguishes queued from in-progress, so a DB-level pending stage was redundant) and renames deleting/deleted to destroying/destroyed to match BoxState's vocabulary. New enum: creating/ready/destroying/destroyed/error.

Includes a pre-deploy migration that widens volume_state_enum and migrates existing rows; old labels stay defined on the Postgres enum (can't be dropped without a full type swap) but unused — same accepted pattern as the ssh_access table noted in migrations/README.md.

Side benefit: this also fixes a pre-existing gap where a crash mid-creation orphaned the row forever, since the reconciler only re-polled the pending_* states, never creating/deleting themselves. It now polls creating/destroying directly and picks interrupted volumes back up on the next tick.

2. sourcevolume_id — the REST VolumeSpec.source field (scheme-qualified, volume://<id>) is now volume_id (bare id) on the /v1 REST surface. The deprecated host_path fallback is unchanged (still requires the volume:// scheme for existing clients built against it).

3. POST /v1/volumes accepts an optional name — enforced unique within the organization by the existing VolumeService.create() check. The endpoint previously always defaulted to the server-assigned id since it passed an empty DTO regardless of what the caller sent.

Test plan:

  • yarn nx run api:build — clean
  • yarn nx run api:test — 65/67 suites pass; the 2 failures (usage.service.integration.spec.ts, job.service.claim.integration.spec.ts) need a real local Postgres, are unrelated to volumes, and fail identically on main
  • yarn nx run dashboard:build — clean (VolumeState consumers updated)
  • cd apps/libs/api-client-go && go build ./... — clean
  • cargo check -p boxlite --features rest && cargo test rest::types — clean
  • cargo fmt --all -- --check — clean

Summary by CodeRabbit

  • New Features
    • Volumes can now be created with an optional name.
    • Managed volume requests use volume_id, with deprecated host_path compatibility retained.
  • Improvements
    • Simplified volume lifecycle statuses to creating, ready, destroying, destroyed, and error.
    • Volume deletion now clearly reflects in-progress and completed states across the dashboard and API.
    • Destroyed volumes are removed from active listings and can be recreated with the same name.
  • Documentation
    • Updated API schemas and client models to reflect the new request fields and lifecycle statuses.

…ame on REST create

Three related cleanups to the managed-volume feature (boxlite-ai#1191/boxlite-ai#1192/boxlite-ai#1056):

1. VolumeState: drops the pending_create/pending_delete stages (the
   reconciler's per-volume Redis lock already distinguishes queued from
   in-progress, so a DB-level pending stage was redundant) and renames
   deleting/deleted to destroying/destroyed to match BoxState's vocabulary.
   New enum: creating/ready/destroying/destroyed/error. Includes a
   pre-deploy migration that widens volume_state_enum and migrates existing
   rows; old labels stay defined on the Postgres enum (can't be dropped)
   but unused, same pattern as the ssh_access table noted in
   migrations/README.md. As a side effect this also fixes a pre-existing
   gap: the reconciler only polled the old pending_* states, so a crash
   mid-creation orphaned the row forever — it now polls creating/destroying
   directly and picks interrupted volumes back up on the next tick.

2. REST VolumeSpec: source (scheme-qualified, 'volume://<id>') is now
   volume_id (bare id) on the new /v1 REST surface. The deprecated
   host_path fallback is unchanged (still requires the volume:// scheme
   for existing clients built against it).

3. POST /v1/volumes now accepts an optional name, enforced unique within
   the organization by the existing VolumeService.create() check — the
   endpoint previously always defaulted to the server-assigned id since it
   passed an empty DTO.

Test plan:
- yarn nx run api:build — clean
- yarn nx run api:test — 65/67 suites pass; the 2 failures
  (usage.service.integration.spec.ts, job.service.claim.integration.spec.ts)
  need a real local Postgres unrelated to volumes and fail identically on
  main
- yarn nx run dashboard:build — clean (VolumeState consumers updated)
- (cd apps/libs/api-client-go && go build ./...) — clean
- cargo check -p boxlite --features rest && cargo test rest::types — clean
- cargo fmt --all -- --check — clean
@G4614
G4614 requested a review from a team as a code owner August 12, 2026 08:23
@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":"09a3a596-8362-4e27-9ea0-312b0952132f","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":329,"uuid":"a55f97bf-e743-42c0-9e52-4edb26ec96d8"}

stderr:
<empty>

powered by BoxLite

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR simplifies volume lifecycle states, adds named volume creation, renames managed-volume input from source to volume_id, updates compatibility handling, and synchronizes API clients, Rust serialization, dashboard behavior, migrations, and tests.

Changes

Volume lifecycle state simplification

Layer / File(s) Summary
Simplified state contract and migration
apps/api/src/box/..., apps/api/src/migrations/..., openapi/..., apps/libs/api-client*
Volume states replace pending and legacy deletion values with creating, destroying, and destroyed. The migration converts existing rows and updates the default state.
Backend lifecycle processing
apps/api/src/box/managers/..., apps/api/src/box/services/...
Creation and destruction processing uses the active transition states. Completed volumes use DESTROYED and a -destroyed suffix. Lookup and deletion checks exclude destroyed volumes.
Dashboard state handling
apps/dashboard/src/components/..., apps/dashboard/src/hooks/..., apps/dashboard/src/pages/...
Deletion controls, filters, optimistic updates, and WebSocket cache removal use DESTROYING and DESTROYED.

REST volume contract updates

Layer / File(s) Summary
Named volume creation
openapi/box.openapi.yaml, apps/api/src/boxlite-rest/...
Volume creation accepts an optional non-empty name. The controller passes the request body to VolumeService.create.
Managed-volume identifier mapping
apps/api/src/boxlite-rest/dto/..., apps/api/src/boxlite-rest/mappers/..., src/boxlite/src/rest/types.rs
Managed volumes use volume_id. Deprecated host_path remains valid only with the volume:// scheme. Rust serialization now sends a bare identifier.

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

Sequence Diagram(s)

sequenceDiagram
  participant VolumeClient
  participant BoxliteVolumeController
  participant VolumeService
  participant VolumeManager
  participant Dashboard
  VolumeClient->>BoxliteVolumeController: submit named volume request
  BoxliteVolumeController->>VolumeService: create volume with name
  VolumeService->>VolumeManager: persist CREATING volume
  Dashboard->>VolumeService: request deletion
  VolumeService->>VolumeManager: process DESTROYING volume
  VolumeManager-->>Dashboard: publish DESTROYED state
Loading

Possibly related PRs

Suggested reviewers: dorianzheng

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes and verification steps but omits the required Before/After call graph and template section headings. Add the required Summary, Call graph with Before and After hops, Changes, and How to verify sections; retain the existing technical details.
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the three primary changes: volume state simplification, the volume_id rename, and optional REST volume names.
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
🧪 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: 5

Caution

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

⚠️ Outside diff range comments (1)
apps/api/src/box/managers/volume.manager.ts (1)

199-209: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Treat BucketAlreadyOwnedByYou as success during creation recovery.

When a resumed CREATING volume already has its bucket, S3 returns this error in Regions other than us-east-1. The current catch block changes the volume to ERROR before applying tags or saving READY. Handle this error as success, then apply tags and persist READY.

🤖 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 `@apps/api/src/box/managers/volume.manager.ts` around lines 199 - 209, Update
handleCreating to catch and ignore the S3 BucketAlreadyOwnedByYou error from
CreateBucketCommand, treating it as successful creation. Continue through tag
application and persist the volume as READY for this case, while preserving
ERROR handling for all other failures.
🤖 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
`@apps/api/src/migrations/pre-deploy/1786200000000-simplify-volume-state-migration.ts`:
- Around line 17-26: The migration currently adds PostgreSQL enum values and
uses them within the same transaction, which is unsupported. Configure TypeORM
for per-migration transaction control, mark the enum-addition queries in the
migration’s `up` method as non-transactional, and move the volume state updates
plus default change into a later migration so the new values are committed
before use.

In `@apps/dashboard/src/components/VolumeTable.tsx`:
- Line 193: Apply isVolumeDeletable(row.original) to the per-row checkbox and
delete-menu action so DESTROYED and DESTROYING volumes cannot be selected or
activated, using disabled semantics rather than only pointer-events styling. In
the delete action handler, re-check isVolumeDeletable before invoking onDelete,
while preserving processingVolumeAction handling for in-progress operations.

In `@openapi/box.openapi.yaml`:
- Around line 1829-1845: Add an anyOf constraint to the filesystem mount schema
alongside the existing required guest_path declaration, requiring either
volume_id or the deprecated host_path property while preserving their current
definitions and compatibility behavior.
- Around line 197-201: Update the requestBody definition for the
CreateVolumeRequest schema to set required: true, ensuring clients must provide
a JSON body before the controller passes it to VolumeService.create.

In `@src/boxlite/src/rest/types.rs`:
- Around line 226-234: Normalize managed volume sources in the REST conversion
implemented by From<&VolumeSpec> for CreateBoxVolumeSpec so volume_id contains
only the bare ID, regardless of whether host_path is volume://<id> or already
normalized. Preserve non-managed paths unchanged, and add regression coverage
for both prefixed SDK inputs and already-bare IDs.

---

Outside diff comments:
In `@apps/api/src/box/managers/volume.manager.ts`:
- Around line 199-209: Update handleCreating to catch and ignore the S3
BucketAlreadyOwnedByYou error from CreateBucketCommand, treating it as
successful creation. Continue through tag application and persist the volume as
READY for this case, while preserving ERROR handling for all other failures.
🪄 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: 6b2fa48d-2d9d-421a-971b-9303972c5258

📥 Commits

Reviewing files that changed from the base of the PR and between 077fe4f and 2550491.

📒 Files selected for processing (23)
  • apps/api/src/box/entities/volume.entity.ts
  • apps/api/src/box/enums/volume-state.enum.ts
  • apps/api/src/box/managers/volume.manager.ts
  • apps/api/src/box/services/volume.service.spec.ts
  • apps/api/src/box/services/volume.service.ts
  • apps/api/src/boxlite-rest/boxlite-volume.controller.spec.ts
  • apps/api/src/boxlite-rest/boxlite-volume.controller.ts
  • apps/api/src/boxlite-rest/dto/create-box.dto.spec.ts
  • apps/api/src/boxlite-rest/dto/create-box.dto.ts
  • apps/api/src/boxlite-rest/dto/create-volume.dto.ts
  • apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts
  • apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts
  • apps/api/src/migrations/pre-deploy/1786200000000-simplify-volume-state-migration.ts
  • apps/dashboard/src/components/VolumeTable.tsx
  • apps/dashboard/src/components/VolumeTable/useVolumeCommands.tsx
  • apps/dashboard/src/hooks/useVolumeWsSync.ts
  • apps/dashboard/src/pages/Volumes.tsx
  • apps/libs/api-client-go/api/openapi.yaml
  • apps/libs/api-client-go/model_volume_state.go
  • apps/libs/api-client/src/docs/VolumeState.md
  • apps/libs/api-client/src/models/volume-state.ts
  • openapi/box.openapi.yaml
  • src/boxlite/src/rest/types.rs

Comment on lines +17 to +26
await queryRunner.query(`ALTER TYPE "public"."volume_state_enum" ADD VALUE IF NOT EXISTS 'destroying'`)
await queryRunner.query(`ALTER TYPE "public"."volume_state_enum" ADD VALUE IF NOT EXISTS 'destroyed'`)

await queryRunner.query(`UPDATE "volume" SET "state" = 'creating' WHERE "state" = 'pending_create'`)
await queryRunner.query(
`UPDATE "volume" SET "state" = 'destroying' WHERE "state" IN ('pending_delete', 'deleting')`,
)
await queryRunner.query(`UPDATE "volume" SET "state" = 'destroyed' WHERE "state" = 'deleted'`)

await queryRunner.query(`ALTER TABLE "volume" ALTER COLUMN "state" SET DEFAULT 'creating'`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify that migration execution is explicitly non-transactional before
# relying on newly-added PostgreSQL enum values in the same migration.
rg -n -C 3 'migrationsTransactionMode|transaction\s*=' apps \
  -g '*.ts' -g '*.js'

sed -n '1,80p' apps/api/src/migrations/pre-deploy/1786200000000-simplify-volume-state-migration.ts

Repository: boxlite-ai/boxlite

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- migration ---'
sed -n '1,140p' apps/api/src/migrations/pre-deploy/1786200000000-simplify-volume-state-migration.ts

printf '%s\n' '--- migration configuration and runners ---'
rg -n -C 4 'migrationsTransactionMode|transaction\s*[:=]|runMigrations|MigrationExecutor|pre-deploy|migrations' apps \
  -g '*.ts' -g '*.js' -g '*.json' -g '*.yml' -g '*.yaml' || true

printf '%s\n' '--- relevant package metadata ---'
rg -n -C 3 '"typeorm"|"migration"' apps package.json package-lock.json npm-shrinkwrap.json yarn.lock pnpm-lock.yaml 2>/dev/null || true

Repository: boxlite-ai/boxlite

Length of output: 21855


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- datasource options ---'
sed -n '1,75p' apps/api/src/migrations/data-source.ts
sed -n '65,115p' apps/api/src/main.ts
sed -n '60,100p' apps/api/src/app.module.ts

printf '%s\n' '--- all migration transaction settings ---'
rg -n -C 5 'migrations\s*:\s*\{|transaction\s*[:=]|MigrationExecutor|setOptions\s*\(' . \
  -g '*.ts' -g '*.js' -g '*.json' -g '!node_modules' -g '!dist' -g '!build' || true

printf '%s\n' '--- TypeORM version and local source availability ---'
rg -n -C 2 '"typeorm"\s*:' apps/package.json package.json
fd -HI 'migration-executor|MigrationExecutor' . -t f -E node_modules -E dist -E build || true

Repository: boxlite-ai/boxlite

Length of output: 13935


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

migration = Path("apps/api/src/migrations/pre-deploy/1786200000000-simplify-volume-state-migration.ts").read_text()
datasource = Path("apps/api/src/migrations/data-source.ts").read_text()
predeploy = Path("apps/api/src/migrations/pre-deploy/data-source.ts").read_text()
app = Path("apps/api/src/app.module.ts").read_text()

add_destroying = migration.index("ADD VALUE IF NOT EXISTS 'destroying'")
use_destroying = migration.index("SET \"state\" = 'destroying'")
add_destroyed = migration.index("ADD VALUE IF NOT EXISTS 'destroyed'")
use_destroyed = migration.index("SET \"state\" = 'destroyed'")

print({
    "destroying_added_before_use": add_destroying < use_destroying,
    "destroyed_added_before_use": add_destroyed < use_destroyed,
    "datasource_transaction_option": bool(re.search(r"migrations\s*:\s*\{[^}]*transaction|migrationsTransactionMode", datasource, re.S)),
    "predeploy_transaction_option": bool(re.search(r"migrations\s*:\s*\{[^}]*transaction|migrationsTransactionMode", predeploy, re.S)),
    "app_transaction_option": bool(re.search(r"migrations\s*:\s*\{[^}]*transaction|migrationsTransactionMode", app, re.S)),
})
PY

Repository: boxlite-ai/boxlite

Length of output: 342


🌐 Web query:

PostgreSQL ALTER TYPE ADD VALUE new enum value cannot be used until after transaction commit official documentation

💡 Result:

According to the official PostgreSQL documentation, if the ALTER TYPE... ADD VALUE command is executed within a transaction block, the new enum value cannot be used until that transaction has been committed [1][2][3]. This restriction exists because the database must prevent uncommitted enum values from being stored in tables, which could potentially break indexes on enum columns [4]. Historical versions of PostgreSQL (prior to version 12) strictly disallowed the execution of this command entirely within transaction blocks; however, this restriction was subsequently relaxed to allow the command, provided the new value is not referenced until after the transaction commits [5][4].

Citations:


🌐 Web query:

TypeORM 0.3 migrations transaction default all official documentation

💡 Result:

In TypeORM 0.3, the default behavior for running migrations is to wrap all migrations into a single transaction [1][2][3]. This is controlled by the migrationsTransactionMode setting, which defaults to all [1][4]. The available options for migrationsTransactionMode are: - all (default): Wraps the entire migration process into a single transaction [1][4]. - each: Wraps each individual migration file into its own separate transaction [1][4]. - none: Disables automatic transaction wrapping for migrations [1][4]. In addition to the global migrationsTransactionMode setting, you can override transaction behavior on a per-migration basis by setting the transaction property (a boolean) within your migration class [2][3]. Note that this per-migration override only functions when the global mode is set to each or none [2][3].

Citations:


Commit enum values before using them.

Configure TypeORM with per-migration transactions. Run the enum additions with transaction = false, then run the updates and default change in a later migration.

🤖 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
`@apps/api/src/migrations/pre-deploy/1786200000000-simplify-volume-state-migration.ts`
around lines 17 - 26, The migration currently adds PostgreSQL enum values and
uses them within the same transaction, which is unsupported. Configure TypeORM
for per-migration transaction control, mark the enum-addition queries in the
migration’s `up` method as non-transactional, and move the volume state updates
plus default change into a later migration so the new values are committed
before use.

key={row.id}
data-state={row.getIsSelected() && 'selected'}
className={`${processingVolumeAction[row.original.id] || row.original.state === VolumeState.PENDING_DELETE || row.original.state === VolumeState.DELETING ? 'opacity-50 pointer-events-none' : ''}`}
className={`${processingVolumeAction[row.original.id] || row.original.state === VolumeState.DESTROYING ? 'opacity-50 pointer-events-none' : ''}`}

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

Apply deletion eligibility to every row control.

The new checks protect header selection, but the per-row checkbox still accepts DESTROYED rows. The delete menu also checks only processingVolumeAction. pointer-events-none does not provide a semantic or keyboard guard for DESTROYING.

Use isVolumeDeletable(row.original) to disable the row checkbox and delete action. Re-check the predicate before calling onDelete. Otherwise, stale terminal rows can be selected or submitted for deletion, and bulk confirmation can silently discard them.

Also applies to: 310-313

🤖 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 `@apps/dashboard/src/components/VolumeTable.tsx` at line 193, Apply
isVolumeDeletable(row.original) to the per-row checkbox and delete-menu action
so DESTROYED and DESTROYING volumes cannot be selected or activated, using
disabled semantics rather than only pointer-events styling. In the delete action
handler, re-check isVolumeDeletable before invoking onDelete, while preserving
processingVolumeAction handling for in-progress operations.

Comment thread openapi/box.openapi.yaml
Comment on lines +197 to +201
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateVolumeRequest"

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 '`@Body`\(\).*CreateVolumeDto|createVolumeDto\.name|CreateVolumeRequest' \
  apps/api/src openapi/box.openapi.yaml \
  -g '*.ts' -g '*.yaml'

Repository: boxlite-ai/boxlite

Length of output: 4246


Require the JSON body.

requestBody omits required, so clients may omit it. The controller passes @Body() directly to VolumeService.create, which evaluates createVolumeDto.name; an omitted body can cause a runtime error. Add required: true, or normalize an absent body before calling the service.

🤖 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 `@openapi/box.openapi.yaml` around lines 197 - 201, Update the requestBody
definition for the CreateVolumeRequest schema to set required: true, ensuring
clients must provide a JSON body before the controller passes it to
VolumeService.create.

Comment thread openapi/box.openapi.yaml
Comment on lines +1829 to +1845
Filesystem mount attached to a box. One of `volume_id` or the
deprecated `host_path` is required.
required: [guest_path]
properties:
source:
volume_id:
type: string
description: |
Scheme-qualified mount source. Use `volume://<volume_id>` for managed
volumes.
example: volume://vol_01K2EXAMPLE
description: Id of a managed volume created via `POST /volumes`.
example: vol_01K2EXAMPLE
host_path:
type: string
deprecated: true
description: |
Deprecated alias for `source`, kept for backward compatibility with
existing /v1 clients built against the pre-managed-volumes schema.
Use `source` with the `volume://<volume_id>` scheme instead; this
field will be removed in a future API version.
Deprecated alias for `volume_id`, kept for backward compatibility
with existing /v1 clients built against the pre-`volume_id`
schema. Still requires the `volume://<volume_id>` scheme those
clients send. Use `volume_id` instead; this field will be removed
in a future API version.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Encode the required managed-volume reference.

The description requires volume_id or host_path, but the schema accepts a VolumeSpec with only guest_path. Add an anyOf constraint so generated clients and validators reject mounts without a volume reference.

Proposed schema fix
 VolumeSpec:
   type: object
+  anyOf:
+    - required: [volume_id]
+    - required: [host_path]
   required: [guest_path]
📝 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.

Suggested change
Filesystem mount attached to a box. One of `volume_id` or the
deprecated `host_path` is required.
required: [guest_path]
properties:
source:
volume_id:
type: string
description: |
Scheme-qualified mount source. Use `volume://<volume_id>` for managed
volumes.
example: volume://vol_01K2EXAMPLE
description: Id of a managed volume created via `POST /volumes`.
example: vol_01K2EXAMPLE
host_path:
type: string
deprecated: true
description: |
Deprecated alias for `source`, kept for backward compatibility with
existing /v1 clients built against the pre-managed-volumes schema.
Use `source` with the `volume://<volume_id>` scheme instead; this
field will be removed in a future API version.
Deprecated alias for `volume_id`, kept for backward compatibility
with existing /v1 clients built against the pre-`volume_id`
schema. Still requires the `volume://<volume_id>` scheme those
clients send. Use `volume_id` instead; this field will be removed
in a future API version.
Filesystem mount attached to a box. One of `volume_id` or the
deprecated `host_path` is required.
anyOf:
- required: [volume_id]
- required: [host_path]
required: [guest_path]
properties:
volume_id:
type: string
description: Id of a managed volume created via `POST /volumes`.
example: vol_01K2EXAMPLE
host_path:
type: string
deprecated: true
description: |
Deprecated alias for `volume_id`, kept for backward compatibility
with existing /v1 clients built against the pre-`volume_id`
schema. Still requires the `volume://<volume_id>` scheme those
clients send. Use `volume_id` instead; this field will be removed
in a future API version.
🤖 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 `@openapi/box.openapi.yaml` around lines 1829 - 1845, Add an anyOf constraint
to the filesystem mount schema alongside the existing required guest_path
declaration, requiring either volume_id or the deprecated host_path property
while preserving their current definitions and compatibility behavior.

Comment on lines 226 to 234
impl From<&crate::runtime::options::VolumeSpec> for CreateBoxVolumeSpec {
fn from(volume: &crate::runtime::options::VolumeSpec) -> Self {
Self {
source: managed_volume_source(&volume.host_path),
// `--mount src=volume://<id>,...` already strips the scheme
// before storing the bare id in `host_path` (see
// `normalize_managed_volume_source` in the CLI) — the wire field
// takes that bare id directly, no scheme wrapping needed.
volume_id: volume.host_path.clone(),
guest_path: volume.guest_path.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the type and normalization helpers before inspecting call sites.
ast-grep outline src/boxlite/src/runtime/options.rs --items all

# Find VolumeSpec constructors, managed-volume normalization, and persisted forms.
rg -n -U -C 4 --type rust \
  'VolumeSpec\s*\{|host_path\s*:|normalize_managed_volume_source|volume://' \
  src/boxlite/src

Repository: boxlite-ai/boxlite

Length of output: 22437


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Trace managed-volume parsing and every path that creates or converts VolumeSpec.
rg -n -U -C 6 --type rust \
  'normalize_managed_volume_source|volume://|VolumeSpec\s*\{|CreateBoxVolumeSpec|\.volumes|volumes\s*:' \
  .

# Inspect the exact implementations for the discovered normalization and conversion paths.
files=$(rg -l --type rust \
  'normalize_managed_volume_source|CreateBoxVolumeSpec|VolumeSpec\s*\{' . | head -20)
for file in $files; do
  echo "===== $file ====="
  sed -n '1,260p' "$file" | grep -n -E \
    -B 12 -A 28 'normalize_managed_volume_source|CreateBoxVolumeSpec|VolumeSpec\s*\{' || true
done

Repository: boxlite-ai/boxlite

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -U -C 6 --type rust \
  'normalize_managed_volume_source|volume://|VolumeSpec\s*\{|CreateBoxVolumeSpec|\.volumes|volumes\s*:' .

files=$(rg -l --type rust \
  'normalize_managed_volume_source|CreateBoxVolumeSpec|VolumeSpec\s*\{' . | head -20)
for file in $files; do
  echo "===== $file ====="
  sed -n '1,260p' "$file" | grep -n -E \
    -B 12 -A 28 'normalize_managed_volume_source|CreateBoxVolumeSpec|VolumeSpec\s*\{' || true
done

Repository: boxlite-ai/boxlite

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the complete conversion paths that preserve scheme-qualified managed sources.
sed -n '250,305p' sdks/node/src/options.rs
sed -n '585,655p' sdks/python/src/options.rs
sed -n '635,685p' sdks/python/src/options.rs
sed -n '930,1002p' src/cli/src/cli.rs

# Locate the REST conversion call sites and the backend path that sends BoxOptions.
rg -n -U -C 8 --type rust \
  'CreateBoxVolumeSpec|BoxOptions.*volumes|volumes.*CreateBox|create_box|create\(' \
  src/boxlite/src/rest src/boxlite/src/runtime src/cli/src sdks/node/src sdks/python/src

Repository: boxlite-ai/boxlite

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '250,305p' sdks/node/src/options.rs
sed -n '585,655p' sdks/python/src/options.rs
sed -n '635,685p' sdks/python/src/options.rs
sed -n '930,1002p' src/cli/src/cli.rs

rg -n -U -C 8 --type rust \
  'CreateBoxVolumeSpec|BoxOptions.*volumes|volumes.*CreateBox|create_box|create\(' \
  src/boxlite/src/rest src/boxlite/src/runtime src/cli/src sdks/node/src sdks/python/src

Repository: boxlite-ai/boxlite

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read-only source-level verifier for the representation invariant and REST mapping.
python3 - <<'PY'
from pathlib import Path

node = Path("sdks/node/src/options.rs").read_text()
python = Path("sdks/python/src/options.rs").read_text()
cli = Path("src/cli/src/cli.rs").read_text()
rest = Path("src/boxlite/src/rest/types.rs").read_text()

checks = {
    "Node accepts scheme-qualified source": 'if !source.starts_with("volume://")' in node,
    "Node stores source unchanged": "host_path,\n            guest_path: v.guest_path" in node,
    "Python accepts scheme-qualified source": 'if !source.starts_with("volume://")' in python,
    "Python stores source unchanged": "host_path: v.host" in python,
    "CLI strips scheme": 'source.strip_prefix("volume://")' in cli,
    "REST copies host_path directly": "volume_id: volume.host_path.clone()" in rest,
    "REST maps every BoxOptions volume": ".map(CreateBoxVolumeSpec::from)" in rest,
}

for name, result in checks.items():
    print(f"{name}: {'PASS' if result else 'FAIL'}")

assert all(checks.values())

# Model the relevant transformations without executing repository code.
scheme = "volume://vol_123"
cli_id = scheme.removeprefix("volume://")
sdk_id = scheme
wire_from_cli = cli_id
wire_from_sdk = sdk_id

print(f"CLI host_path -> wire volume_id: {wire_from_cli!r}")
print(f"SDK host_path -> wire volume_id: {wire_from_sdk!r}")
assert wire_from_cli == "vol_123"
assert wire_from_sdk == "volume://vol_123"
assert wire_from_sdk != "vol_123"
PY

Repository: boxlite-ai/boxlite

Length of output: 512


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

node = Path("sdks/node/src/options.rs").read_text()
python = Path("sdks/python/src/options.rs").read_text()
cli = Path("src/cli/src/cli.rs").read_text()
rest = Path("src/boxlite/src/rest/types.rs").read_text()

checks = {
    "Node accepts scheme-qualified source": 'if !source.starts_with("volume://")' in node,
    "Node stores source unchanged": "host_path,\n            guest_path: v.guest_path" in node,
    "Python accepts scheme-qualified source": 'if !source.starts_with("volume://")' in python,
    "Python stores source unchanged": "host_path: v.host" in python,
    "CLI strips scheme": 'source.strip_prefix("volume://")' in cli,
    "REST copies host_path directly": "volume_id: volume.host_path.clone()" in rest,
    "REST maps every BoxOptions volume": ".map(CreateBoxVolumeSpec::from)" in rest,
}

for name, result in checks.items():
    print(f"{name}: {'PASS' if result else 'FAIL'}")

assert all(checks.values())

scheme = "volume://vol_123"
cli_id = scheme.removeprefix("volume://")
sdk_id = scheme

print(f"CLI host_path -> wire volume_id: {cli_id!r}")
print(f"SDK host_path -> wire volume_id: {sdk_id!r}")

assert cli_id == "vol_123"
assert sdk_id == "volume://vol_123"
assert sdk_id != "vol_123"
PY

Repository: boxlite-ai/boxlite

Length of output: 512


Normalize managed volume sources before REST serialization

Node and Python adapters preserve volume://<id> in VolumeSpec.host_path. The REST mapper then sends that value unchanged as volume_id. Normalize these SDK inputs, or strip the scheme in the REST mapper, and add regression tests for both paths.

🤖 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/rest/types.rs` around lines 226 - 234, Normalize managed
volume sources in the REST conversion implemented by From<&VolumeSpec> for
CreateBoxVolumeSpec so volume_id contains only the bare ID, regardless of
whether host_path is volume://<id> or already normalized. Preserve non-managed
paths unchanged, and add regression coverage for both prefixed SDK inputs and
already-bare IDs.

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