feat(box): image pipeline + single box identity (drop dual boxId)#756
feat(box): image pipeline + single box identity (drop dual boxId)#756law-chain-hot wants to merge 1 commit into
Conversation
Backend-only image/identity refactor against main — no dashboard cloudBox migration, no TS SDK package removal (those stay in #748). - Image: first-class `Box.image` column, full OCI ref end-to-end (no curated-key translation), runner contract field `image`, e2e adapted. - Single identity: collapse the dual uuid/boxId into one 12-char base62 id (the PK). The internal engine id is never exposed; `boxId` (the redundant duplicate of the cloud id) is dropped everywhere. - Regenerate the 4 API clients (CI-faithful): Box/AdminBoxItem/Workspace lose boxId, gain image. - Consumers of the dropped boxId fixed in place (NOT the SDK removal): dashboard reads switch boxId->id (6 files); sdk-typescript Box mirrors the new api-client shape (drop boxId field, add image). The SDK package itself is untouched — its removal + the cloudBox dashboard migration remain #748. Verified: SDK build OK, dashboard tsc clean, API jest 112/112, go vet clean (runner pkg), engine unchanged vs main.
📝 WalkthroughWalkthroughThis PR consolidates box identity from a dual-field model (internal ChangesBox Identity Refactor and Image Allowlisting
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
|
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
apps/api-client-go/api/openapi.yaml (2)
10216-10321:⚠️ Potential issue | 🟠 Major | ⚡ Quick winExpose
imageonCreateBox.The PR moves image selection into the request contract, but
CreateBoxstill has noimagefield at all. Generated clients therefore can't submit the OCI ref that the backend now persists and forwards toCREATE_BOX.Suggested schema shape
env: additionalProperties: type: string description: Environment variables for the box example: NODE_ENV: production type: object + image: + description: The OCI image ref the box boots from + example: ghcr.io/boxlite-ai/boxlite-agent-python@sha256:80d562a57f4bc12def4e54dbdb9e7d26d3268fe0767a2955ab5ad718041145d6 + type: string labels: additionalProperties: type: stringMirror the required/optional status from
CreateBoxDtowhen regenerating the clients.🤖 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-client-go/api/openapi.yaml` around lines 10216 - 10321, The CreateBox schema in openapi.yaml is missing the image property used by the backend; add an "image" property to the CreateBox object (matching the required/optional status from the server-side CreateBoxDto), with type string and an example containing an OCI ref, and then regenerate clients so they can submit the OCI image ref that the backend expects; update the CreateBox schema block and ensure the volumes/items and other existing fields remain unchanged.
9938-9979:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winThe public examples/docs still advertise the removed UUID-based identity model. This rollout switches public box/workspace ids to the single exposed id, but these schemas/examples still describe or show UUID/internal ids.
apps/api-client-go/api/openapi.yaml#L9938-L9979: update theBox.iddescription/example to the new public id semantics.apps/api-client-go/api/openapi.yaml#L10121-L10196: change the paginated box example items to use the new public id shape.apps/api-client-go/api/openapi.yaml#L12487-L12529: update theWorkspace.iddescription/example to the new public id semantics.🤖 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-client-go/api/openapi.yaml` around lines 9938 - 9979, Update the OpenAPI examples and descriptions to use the new public id semantics instead of UUID/internal ids: in apps/api-client-go/api/openapi.yaml (lines 9938-9979) change the Box.id description to state it's the public exposed ID (not an internal UUID) and replace the example value (fd955d93-...) with a representative public id (e.g., "box_abc123" or the project's public id format) and update any adjacent example fields that reference the internal UUID; in apps/api-client-go/api/openapi.yaml (lines 10121-10196) update the paginated boxes example items so each item uses the public id shape/value (replace UUID examples with the public id format) and adjust any descriptive text that mentions "internal UUID"; and in apps/api-client-go/api/openapi.yaml (lines 12487-12529) change the Workspace.id description/example to the new public id semantics and swap the UUID example for a representative public workspace id (e.g., "workspace_xyz789"). Ensure the schema names Box.id and Workspace.id are the ones updated so examples and generated docs reflect the public id model consistently.apps/api/src/box/dto/box.dto.ts (1)
38-41:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate
idschema metadata to the new single-ID format.The
idfield is now the public 12-character base62 identifier, but the Swagger description/example still describe a UUID. Please update this metadata so published docs and generated examples match the new contract.Suggested update
`@ApiProperty`({ - description: 'The internal UUID of the box', - example: 'fd955d93-e74a-48e7-9f2d-fcbe6dd9e920', + description: 'The public 12-character base62 ID of the box', + example: 'aB3dE6fGh1Jk', }) id: string🤖 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/dto/box.dto.ts` around lines 38 - 41, Update the `@ApiProperty` metadata for the id field in the Box DTO to reflect the new public 12-character base62 identifier (not a UUID): change the description to mention "public 12-character base62 ID" and replace the UUID example with a representative 12-char base62 example (e.g. "A1b2C3d4E5f6") so Swagger docs and generated examples match the new contract; locate the `@ApiProperty` decorator applied to the id property in the DTO (box.dto.ts) and edit its description and example accordingly.apps/api/src/box/repositories/box.repository.ts (1)
84-87:⚠️ Potential issue | 🟠 Major | ⚡ Quick winEnforce immutable
idin repository updates.The update paths still accept
idinsideupdateData, but cache invalidation now assumes a stable ID. Ifidis mutated, stale lookup entries and inconsistent related records are possible. Reject or stripidin bothupdateandupdateWhere.Suggested hardening
async update(id: string, params: { updateData: Partial<Box>; entity?: Box }, raw = false): Promise<Box | void> { const { updateData, entity } = params + if (updateData.id !== undefined && updateData.id !== id) { + throw new BoxConflictError() + } + const { id: _ignoredId, ...safeUpdateData } = updateData if (raw) { - await this.repository.update(id, updateData) + await this.repository.update(id, safeUpdateData) return } const box = entity ?? (await this.findOneBy({ id })) @@ - Object.assign(box, updateData) + Object.assign(box, safeUpdateData) @@ - { ...updateData, ...invariantChanges }, + { ...safeUpdateData, ...invariantChanges }, ) @@ async updateWhere( @@ - const { updateData, whereCondition } = params + const { updateData, whereCondition } = params + if (updateData.id !== undefined && updateData.id !== id) { + throw new BoxConflictError() + } + const { id: _ignoredId, ...safeUpdateData } = updateData @@ - Object.assign(box, updateData) + Object.assign(box, safeUpdateData) @@ - await entityManager.update(Box, id, { ...updateData, ...invariantChanges }) + await entityManager.update(Box, id, { ...safeUpdateData, ...invariantChanges })Also applies to: 100-101, 157-161, 202-213
🤖 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/repositories/box.repository.ts` around lines 84 - 87, The repository update handlers must enforce immutable IDs: in the update and updateWhere code paths (where Object.assign(box, updateData) is used and where updateData is applied to items), detect if updateData contains an id and either strip it out before applying or reject the request with an error; update the logic that creates previousBox and the subsequent cache invalidation to rely on the original box.id (do not allow updateData.id to overwrite box.id), and ensure box.assertValid() runs after applying only permitted fields. Apply the same check/strip in both update and updateWhere so stale cache entries or inconsistent relations cannot be introduced.apps/api/src/box/services/box.service.ts (1)
463-469:⚠️ Potential issue | 🟠 MajorMake
filters.idprefix match case-sensitive forbox.id(keep case-insensitive only forbox.name).
apps/api/src/box/services/box.service.tsbuildsidFilter = ILike(${id}%)and uses it for bothidandname(lines 463-474). Box IDs are 12-char base62 using bothA-Zanda-z, so PostgresILike(case-insensitive) can return incorrect boxes for an ID-prefix search.Fix: split the filters—use
Like(${id}%)forbox.idprefix matching, and keepILike(${id}%)for prefix matchingbox.namewhenfilters.idis provided.🤖 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/services/box.service.ts` around lines 463 - 469, The code currently sets idFilter = ILike(`${id}%`) and applies it to both the id and name filters; change this so box id prefix matching is case-sensitive by creating idFilter = Like(`${id}%`) for the id property, while keeping nameFilter as ILike(`${id}%`) for the name property; update the return object that spreads baseFindOptions and nameFilter to set id: idFilter (and ensure any variable names used in the surrounding function/method that reference idFilter, nameFilter, baseFindOptions are adjusted accordingly so id uses Like and name uses ILike).
🧹 Nitpick comments (3)
apps/dashboard/src/hooks/useBoxWsSync.ts (1)
51-54: ⚡ Quick winRedundant duplicate cache update.
After removing the
boxIdfield, bothboxId(the hook parameter) andbox.id(from the event payload) resolve to the same identifier whenmatchesActiveBoxpasses. CallingupdateStateInDetailCachetwice with identical keys is wasteful.♻️ Suggested fix
const optimisticUpdate = (box: Box, state: BoxState) => { updateStateInListCache(box.id, state) if (boxId) { - updateStateInDetailCache(boxId, state) - updateStateInDetailCache(box.id, state) + updateStateInDetailCache(box.id, state) } }🤖 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/hooks/useBoxWsSync.ts` around lines 51 - 54, In useBoxWsSync, remove the redundant cache update: when matchesActiveBox is true boxId and box.id are identical, so delete the extra call to updateStateInDetailCache(boxId, state) and keep a single updateStateInDetailCache(box.id, state) (or vice-versa, but standardize to one identifier) to avoid duplicate writes.scripts/test/e2e/bootstrap.sh (1)
146-150: ⚡ Quick winPin local allowlist images by digest to avoid supply-chain drift.
Using mutable tags (
alpine:3.23,python:3.11-alpine,node:18-alpine) makes e2e behavior non-deterministic as upstream tags move. Prefer digest-pinned refs here too, even for public images, so local runs stay reproducible and safer.🤖 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 `@scripts/test/e2e/bootstrap.sh` around lines 146 - 150, The three mutable image variables (BOXLITE_SYSTEM_BASE_IMAGE, BOXLITE_SYSTEM_PYTHON_IMAGE, BOXLITE_SYSTEM_NODE_IMAGE) should be pinned to immutable digests to prevent supply-chain drift; replace the tag references (alpine:3.23, python:3.11-alpine, node:18-alpine) with their corresponding sha256@digest image references (e.g., alpine:3.23@sha256:<digest>) by resolving the current trusted digests from the official registries and updating each variable to that digest-pinned ref so local e2e runs remain reproducible.apps/api/src/box/runner-adapter/runnerAdapter.v2.createBox.spec.ts (1)
67-70: ⚡ Quick winAdd a
gpuQuotaassertion in the create payload test.The suite validates CPU/memory/storage mappings but leaves GPU unmapped/untested. Add
expect(payload.gpuQuota).toBe(0)(or the configured value) in this block so create-box sizing coverage is complete.🤖 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/runner-adapter/runnerAdapter.v2.createBox.spec.ts` around lines 67 - 70, The create-box test is missing an assertion for GPU sizing; in the test block that asserts payload.cpuQuota, payload.memoryQuota, and payload.storageQuota, add an assertion for the GPU field such as expect(payload.gpuQuota).toBe(0) (or the configured GPU value) so the create payload mapping covers GPU quota as well.
🤖 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-client-go/api/openapi.yaml`:
- Around line 13469-13472: The OpenAPI field organizationId has a mismatched
description ("Public box ID shown to users"); update the description for the
organizationId property in the OpenAPI schema to accurately describe the
organization identifier (e.g., "Public organization ID shown to users" or
"Organization ID exposed to clients") so it matches the field name and example
and avoids confusion in generated admin API docs.
In `@apps/api-client-go/model_admin_box_item.go`:
- Around line 26-27: Update the OpenAPI spec entry
components/schemas/AdminBoxItem/properties/organizationId.description to a
correct description for the organization ID (not “Public box ID shown to
users”), so the generated Go comment on the AdminBoxItem struct field
OrganizationId (in model_admin_box_item.go) reflects the proper meaning; after
changing the description text in apps/api-client-go/api/openapi.yaml, regenerate
the Go client to apply the updated comment.
In `@apps/api/src/box/managers/box-actions/box-start.action.ts`:
- Around line 68-72: Persist the CREATING state before dispatching the external
create operation: move the this.updateBoxState(box, BoxState.CREATING, lockCode)
call to run before runnerAdapter.createBox (or make both actions
atomic/transactional if supported). Concretely, in the box-start handler adjust
the sequence so updateBoxState is awaited first, then call
runnerAdapter.createBox(box); ensure error handling rolls back or prevents
duplicate enqueue on failure and keep the return SYNC_AGAIN behavior unchanged.
In `@apps/api/src/box/runner-adapter/runnerAdapter.v0.ts`:
- Around line 147-150: New boxes can be assigned a V0 runner that throws in
RunnerAdapterV0.createBox(), so update the runner-selection path to only choose
runners that support createBox (e.g., apiVersion === '2') to prevent boxes
moving to ERROR: modify the code that calls
runnerService.getRandomAvailableRunner() for create/start-creation flows (and
any BoxStartAction logic that invokes runnerAdapter.createBox() when BoxState is
UNKNOWN -> desired STARTED) to filter available runners by apiVersion === '2'
(or alternately add a capability check on the selected runner and retry/select
another runner if createBox is unsupported); ensure the check is applied where
runnerAdapter.createBox() is invoked so only adapters that implement createBox
are ever chosen.
In `@apps/api/src/box/runner-adapter/runnerAdapter.v2.ts`:
- Around line 121-134: The CREATE_BOX payload in runnerAdapter.v2.ts is missing
the GPU quota and thus drops box.gpu; update the payload object used to call
createBox to include the GPU field (e.g., add gpuQuota: box.gpu or the runner's
expected key such as gpu: box.gpu) alongside cpuQuota/memoryQuota/storageQuota,
and ensure any related type/interface for the payload (if present) is updated to
include this GPU property so the runner receives and honors the GPU request.
In `@apps/api/src/migrations/1741087887225-migration.ts`:
- Around line 73-74: The CREATE TABLE for "box" currently uses character
varying(12) for the id column but allows shorter/non-base62 values; add a DB
CHECK constraint to enforce exactly 12 base62 chars by adding e.g. CONSTRAINT
box_id_format_chk CHECK (id ~ '^[A-Za-z0-9]{12}$') into the CREATE TABLE
statement (or ALTER TABLE ... ADD CONSTRAINT for an existing table), and also
ensure the migration's down path removes that constraint (DROP CONSTRAINT
box_id_format_chk) so rollbacks remain consistent.
In `@apps/dashboard/src/components/admin/adminHelpers.ts`:
- Around line 223-227: The lookup uses exact-case matching (const targetBoxId =
boxId.trim(); group.boxes.find(b => b.id === targetBoxId)) but callers may
lowercase pasted ids; change the lookup to first attempt an exact match by id
(keep targetBoxId and the current strict compare), and if that returns
undefined, perform a fallback case-insensitive search (compare
b.id.toLowerCase() === targetBoxId.toLowerCase()) so legitimate IDs with
uppercase characters are still found; update the code around targetBoxId and the
group.boxes.find call accordingly.
In `@apps/libs/api-client/src/docs/AdminBoxItem.md`:
- Line 9: The AdminBoxItemDto.organizationId field has conflicting Swagger
descriptions in admin-overview.dto.ts causing incorrect docs; open the DTO in
admin-overview.dto.ts, locate the decorators applied to
AdminBoxItemDto.organizationId and remove or update the duplicate Swagger
decorator so there is a single `@ApiProperty` (or `@ApiPropertyOptional`) whose
description reads "Organization ID" (not "Public box ID shown to users"), then
regenerate the OpenAPI artifacts and clients/docs (so
apps/api-client-go/api/openapi.yaml and
apps/libs/api-client/src/docs/AdminBoxItem.md are updated).
In `@apps/runner/pkg/boxlite/stubs.go`:
- Around line 95-98: The code is hardcoding "alpine:latest" when building
dto.CreateBoxDTO (createDto) during recovery; change it to use the original box
image instead (e.g., set Image to recoverDto.Image or the
originalBox.Image/box.Image field that contains the box's image tag) so the
recovered box boots the same environment; update any callers/tests that assumed
the hardcoded value if needed.
---
Outside diff comments:
In `@apps/api-client-go/api/openapi.yaml`:
- Around line 10216-10321: The CreateBox schema in openapi.yaml is missing the
image property used by the backend; add an "image" property to the CreateBox
object (matching the required/optional status from the server-side
CreateBoxDto), with type string and an example containing an OCI ref, and then
regenerate clients so they can submit the OCI image ref that the backend
expects; update the CreateBox schema block and ensure the volumes/items and
other existing fields remain unchanged.
- Around line 9938-9979: Update the OpenAPI examples and descriptions to use the
new public id semantics instead of UUID/internal ids: in
apps/api-client-go/api/openapi.yaml (lines 9938-9979) change the Box.id
description to state it's the public exposed ID (not an internal UUID) and
replace the example value (fd955d93-...) with a representative public id (e.g.,
"box_abc123" or the project's public id format) and update any adjacent example
fields that reference the internal UUID; in apps/api-client-go/api/openapi.yaml
(lines 10121-10196) update the paginated boxes example items so each item uses
the public id shape/value (replace UUID examples with the public id format) and
adjust any descriptive text that mentions "internal UUID"; and in
apps/api-client-go/api/openapi.yaml (lines 12487-12529) change the Workspace.id
description/example to the new public id semantics and swap the UUID example for
a representative public workspace id (e.g., "workspace_xyz789"). Ensure the
schema names Box.id and Workspace.id are the ones updated so examples and
generated docs reflect the public id model consistently.
In `@apps/api/src/box/dto/box.dto.ts`:
- Around line 38-41: Update the `@ApiProperty` metadata for the id field in the
Box DTO to reflect the new public 12-character base62 identifier (not a UUID):
change the description to mention "public 12-character base62 ID" and replace
the UUID example with a representative 12-char base62 example (e.g.
"A1b2C3d4E5f6") so Swagger docs and generated examples match the new contract;
locate the `@ApiProperty` decorator applied to the id property in the DTO
(box.dto.ts) and edit its description and example accordingly.
In `@apps/api/src/box/repositories/box.repository.ts`:
- Around line 84-87: The repository update handlers must enforce immutable IDs:
in the update and updateWhere code paths (where Object.assign(box, updateData)
is used and where updateData is applied to items), detect if updateData contains
an id and either strip it out before applying or reject the request with an
error; update the logic that creates previousBox and the subsequent cache
invalidation to rely on the original box.id (do not allow updateData.id to
overwrite box.id), and ensure box.assertValid() runs after applying only
permitted fields. Apply the same check/strip in both update and updateWhere so
stale cache entries or inconsistent relations cannot be introduced.
In `@apps/api/src/box/services/box.service.ts`:
- Around line 463-469: The code currently sets idFilter = ILike(`${id}%`) and
applies it to both the id and name filters; change this so box id prefix
matching is case-sensitive by creating idFilter = Like(`${id}%`) for the id
property, while keeping nameFilter as ILike(`${id}%`) for the name property;
update the return object that spreads baseFindOptions and nameFilter to set id:
idFilter (and ensure any variable names used in the surrounding function/method
that reference idFilter, nameFilter, baseFindOptions are adjusted accordingly so
id uses Like and name uses ILike).
---
Nitpick comments:
In `@apps/api/src/box/runner-adapter/runnerAdapter.v2.createBox.spec.ts`:
- Around line 67-70: The create-box test is missing an assertion for GPU sizing;
in the test block that asserts payload.cpuQuota, payload.memoryQuota, and
payload.storageQuota, add an assertion for the GPU field such as
expect(payload.gpuQuota).toBe(0) (or the configured GPU value) so the create
payload mapping covers GPU quota as well.
In `@apps/dashboard/src/hooks/useBoxWsSync.ts`:
- Around line 51-54: In useBoxWsSync, remove the redundant cache update: when
matchesActiveBox is true boxId and box.id are identical, so delete the extra
call to updateStateInDetailCache(boxId, state) and keep a single
updateStateInDetailCache(box.id, state) (or vice-versa, but standardize to one
identifier) to avoid duplicate writes.
In `@scripts/test/e2e/bootstrap.sh`:
- Around line 146-150: The three mutable image variables
(BOXLITE_SYSTEM_BASE_IMAGE, BOXLITE_SYSTEM_PYTHON_IMAGE,
BOXLITE_SYSTEM_NODE_IMAGE) should be pinned to immutable digests to prevent
supply-chain drift; replace the tag references (alpine:3.23, python:3.11-alpine,
node:18-alpine) with their corresponding sha256@digest image references (e.g.,
alpine:3.23@sha256:<digest>) by resolving the current trusted digests from the
official registries and updating each variable to that digest-pinned ref so
local e2e runs remain reproducible.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 58cdc5d5-4ebe-48ce-adc6-562bbfbb424d
📒 Files selected for processing (61)
apps/api-client-go/api/openapi.yamlapps/api-client-go/model_admin_box_item.goapps/api-client-go/model_box.goapps/api-client-go/model_workspace.goapps/api/src/admin/dto/admin-overview.dto.tsapps/api/src/admin/services/observability.service.spec.tsapps/api/src/admin/services/observability.service.tsapps/api/src/admin/services/overview.service.tsapps/api/src/box/constants/curated-images.constant.spec.tsapps/api/src/box/constants/curated-images.constant.tsapps/api/src/box/dto/box.dto.spec.tsapps/api/src/box/dto/box.dto.tsapps/api/src/box/dto/create-box.dto.tsapps/api/src/box/dto/workspace.deprecated.dto.tsapps/api/src/box/entities/box.entity.spec.tsapps/api/src/box/entities/box.entity.tsapps/api/src/box/managers/box-actions/box-start.action.tsapps/api/src/box/repositories/box.repository.tsapps/api/src/box/runner-adapter/runnerAdapter.tsapps/api/src/box/runner-adapter/runnerAdapter.v0.tsapps/api/src/box/runner-adapter/runnerAdapter.v2.createBox.spec.tsapps/api/src/box/runner-adapter/runnerAdapter.v2.tsapps/api/src/box/services/box-lookup-cache-invalidation.service.tsapps/api/src/box/services/box.service.box-id.spec.tsapps/api/src/box/services/box.service.tsapps/api/src/box/services/box.service.warm-pool.spec.tsapps/api/src/box/utils/box-id.util.tsapps/api/src/box/utils/box-lookup-cache.util.tsapps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.tsapps/api/src/boxlite-rest/mappers/box-to-box.mapper.tsapps/api/src/migrations/1741087887225-migration.tsapps/dashboard/src/components/BoxTable/BoxTableActions.tsxapps/dashboard/src/components/admin/AdminPeopleBoxesView.tsxapps/dashboard/src/components/admin/adminDiagnoseTarget.tsapps/dashboard/src/components/admin/adminHelpers.tsapps/dashboard/src/hooks/useBoxWsSync.tsapps/dashboard/src/lib/box-identity.tsapps/libs/api-client/src/docs/AdminBoxItem.mdapps/libs/api-client/src/docs/Box.mdapps/libs/api-client/src/docs/Workspace.mdapps/libs/api-client/src/models/admin-box-item.tsapps/libs/api-client/src/models/box.tsapps/libs/api-client/src/models/workspace.tsapps/libs/sdk-typescript/src/Box.tsapps/runner/pkg/api/dto/box.goapps/runner/pkg/boxlite/client.goapps/runner/pkg/boxlite/stubs.goscripts/test/e2e/README.mdscripts/test/e2e/bootstrap.shscripts/test/e2e/cases/conftest.pyscripts/test/e2e/cases/test_c_entry.pyscripts/test/e2e/cases/test_cli_detach_recovery.pyscripts/test/e2e/cases/test_cli_entry.pyscripts/test/e2e/cases/test_error_code_mapping.pyscripts/test/e2e/cases/test_go_entry.pyscripts/test/e2e/cases/test_node_entry.pyscripts/test/e2e/cases/test_quota_enforcement.pyscripts/test/e2e/fixture_setup.pyscripts/test/e2e/sdks/c/e2e_basic.cscripts/test/e2e/sdks/go/e2e_basic.goscripts/test/e2e/sdks/node/e2e_basic.ts
💤 Files with no reviewable changes (6)
- apps/api/src/admin/services/overview.service.ts
- apps/api/src/box/utils/box-id.util.ts
- apps/api/src/admin/dto/admin-overview.dto.ts
- apps/dashboard/src/components/BoxTable/BoxTableActions.tsx
- apps/api/src/box/utils/box-lookup-cache.util.ts
- apps/api/src/box/dto/workspace.deprecated.dto.ts
| organizationId: | ||
| description: Public box ID shown to users | ||
| example: abc123XYZ | ||
| type: string |
There was a problem hiding this comment.
Fix the organizationId description.
The field name and example are organizationId, but the description still says “Public box ID shown to users”. That will generate contradictory admin API docs.
Suggested doc fix
organizationId:
- description: Public box ID shown to users
+ description: Organization ID that owns the box
example: abc123XYZ
type: string📝 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.
| organizationId: | |
| description: Public box ID shown to users | |
| example: abc123XYZ | |
| type: string | |
| organizationId: | |
| description: Organization ID that owns the box | |
| example: abc123XYZ | |
| type: string |
🤖 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-client-go/api/openapi.yaml` around lines 13469 - 13472, The OpenAPI
field organizationId has a mismatched description ("Public box ID shown to
users"); update the description for the organizationId property in the OpenAPI
schema to accurately describe the organization identifier (e.g., "Public
organization ID shown to users" or "Organization ID exposed to clients") so it
matches the field name and example and avoids confusion in generated admin API
docs.
| // Public box ID shown to users | ||
| BoxId *string `json:"boxId,omitempty"` | ||
| // Organization ID | ||
| OrganizationId string `json:"organizationId"` | ||
| OrganizationId *string `json:"organizationId,omitempty"` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the OpenAPI spec for the AdminBoxItem organizationId description
rg -n -A2 'organizationId' apps/api-client-go/api/openapi.yaml | head -40Repository: boxlite-ai/boxlite
Length of output: 1019
🏁 Script executed:
#!/bin/bash
# Locate AdminBoxItem schema and inspect organizationId description within it.
set -euo pipefail
file="apps/api-client-go/api/openapi.yaml"
echo "== Matches for AdminBoxItem =="
rg -n "AdminBoxItem" "$file" | head -50
echo
echo "== Search for organizationId within the AdminBoxItem area =="
# Try to find the schema block first, then narrow search
# (Assumes description is near 'AdminBoxItem:' or 'AdminBoxItem' definition.)
rg -n "AdminBoxItem" "$file" | head -1 | awk -F: '{print $1}' > /tmp/adminboxitem_line.txt
admin_line=$(cat /tmp/adminboxitem_line.txt)
# Print a window around the first AdminBoxItem occurrence for manual inspection
start=$((admin_line-80))
end=$((admin_line+200))
sed -n "${start},${end}p" "$file" | nl -ba | head -400
echo
echo "== Direct search for description text =="
rg -n "Public box ID shown to users|boxId.*shown to users|shown to users" "$file" || trueRepository: boxlite-ai/boxlite
Length of output: 366
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="apps/api-client-go/api/openapi.yaml"
echo "== AdminBoxItem schema block (around line 13449) =="
start=13380
end=13650
sed -n "${start},${end}p" "$file" | cat -n
echo
echo "== Find organizationId inside AdminBoxItem =="
rg -n "AdminBoxItem:|organizationId:" "$file" | head -50
echo
echo "== Search for the exact stale phrase =="
rg -n "Public box ID shown to users|boxId.*shown to users|shown to users" "$file" || trueRepository: boxlite-ai/boxlite
Length of output: 11210
Fix misleading AdminBoxItem.organizationId description in the OpenAPI spec.
apps/api-client-go/api/openapi.yaml documents components/schemas/AdminBoxItem/properties/organizationId.description as “Public box ID shown to users”, which is the wrong field meaning (it’s organizationId). Update the description so the generated Go comment on OrganizationId (apps/api-client-go/model_admin_box_item.go lines 26-27) matches.
🤖 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-client-go/model_admin_box_item.go` around lines 26 - 27, Update the
OpenAPI spec entry
components/schemas/AdminBoxItem/properties/organizationId.description to a
correct description for the organization ID (not “Public box ID shown to
users”), so the generated Go comment on the AdminBoxItem struct field
OrganizationId (in model_admin_box_item.go) reflects the proper meaning; after
changing the description text in apps/api-client-go/api/openapi.yaml, regenerate
the Go client to apply the updated comment.
| const runnerAdapter = await this.runnerAdapterFactory.create(runner) | ||
| await runnerAdapter.createBox(box) | ||
|
|
||
| await this.updateBoxState(box, BoxState.CREATING, lockCode) | ||
| return SYNC_AGAIN |
There was a problem hiding this comment.
Persist the state transition before (or atomically with) CREATE_BOX dispatch.
Line 68 dispatches createBox before Line 71 persists CREATING. If persistence fails after enqueue succeeds, retries from UNKNOWN can dispatch duplicate create operations for the same box.
Suggested direction
- const runnerAdapter = await this.runnerAdapterFactory.create(runner)
- await runnerAdapter.createBox(box)
-
- await this.updateBoxState(box, BoxState.CREATING, lockCode)
+ await this.updateBoxState(box, BoxState.CREATING, lockCode)
+ const runnerAdapter = await this.runnerAdapterFactory.create(runner)
+ await runnerAdapter.createBox(box) // treat "already exists" as success in adapter path
return SYNC_AGAIN📝 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.
| const runnerAdapter = await this.runnerAdapterFactory.create(runner) | |
| await runnerAdapter.createBox(box) | |
| await this.updateBoxState(box, BoxState.CREATING, lockCode) | |
| return SYNC_AGAIN | |
| await this.updateBoxState(box, BoxState.CREATING, lockCode) | |
| const runnerAdapter = await this.runnerAdapterFactory.create(runner) | |
| await runnerAdapter.createBox(box) // treat "already exists" as success in adapter path | |
| return SYNC_AGAIN |
🤖 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/box-actions/box-start.action.ts` around lines 68 -
72, Persist the CREATING state before dispatching the external create operation:
move the this.updateBoxState(box, BoxState.CREATING, lockCode) call to run
before runnerAdapter.createBox (or make both actions atomic/transactional if
supported). Concretely, in the box-start handler adjust the sequence so
updateBoxState is awaited first, then call runnerAdapter.createBox(box); ensure
error handling rolls back or prevents duplicate enqueue on failure and keep the
return SYNC_AGAIN behavior unchanged.
| async createBox(_box: Box): Promise<void> { | ||
| // V0 (direct HTTP) create is out of MVP scope; only V2 (job-based) create is wired. | ||
| throw new Error('createBox is not supported for V0 runners') | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Create-box call sites =="
rg -nP '\bcreateBox\s*\(' apps/api/src -g '*.ts' -C 2
echo
echo "== Runner version selection / gating =="
rg -nP '\bapiVersion\b|\bRunnerAdapterFactory\b|\.create\(\s*runner' apps/api/src/box -g '*.ts' -C 3
echo
echo "== Any explicit prohibition/upgrade checks for v0 on create =="
rg -nP 'v0|version.?0|Unsupported runner version|not supported for V0|create.*supported' apps/api/src/box -g '*.ts' -C 2Repository: boxlite-ai/boxlite
Length of output: 37768
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate where CREATE transitions happen (create/start orchestration) =="
rg -n "BoxState\.CREATING|CREATE_BOX|createBoxDto|createBox\(" apps/api/src/box -g '*.ts'
echo
echo "== In box-start.action.ts: check runner selection / any apiVersion gating for createBox =="
sed -n '1,240p' apps/api/src/box/managers/box-actions/box-start.action.ts | nl -ba
echo
echo "== In box.service.ts: check createBox / runner selection logic near creates =="
rg -n "createBox\(|startBox\(|BoxState\.CREATING" apps/api/src/box/services/box.service.ts -g '*.ts'
sed -n '760,980p' apps/api/src/box/services/box.service.ts | nl -baRepository: boxlite-ai/boxlite
Length of output: 8820
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== BoxStartAction: find BoxState.CREATING branch and createBox call context =="
rg -n "class BoxStartAction|case BoxState\.CREATING|createBox\(" apps/api/src/box/managers/box-actions/box-start.action.ts
echo
echo "== Print around createBox() call (first ~130 lines where it's referenced) =="
sed -n '1,200p' apps/api/src/box/managers/box-actions/box-start.action.ts | cat -n
echo
echo "== Print later portion around BoxState.CREATING handler (where switch likely continues) =="
sed -n '200,380p' apps/api/src/box/managers/box-actions/box-start.action.ts | cat -n
echo
echo "== BoxService: look for runner selection / apiVersion filtering during create =="
rg -n "createBoxInternal|createFromTemplate|BoxState\.CREATING|runner.*apiVersion|apiVersion.*runner|warmPool|find.*runner|select.*runner" apps/api/src/box/services/box.service.ts -g '*.ts'
echo
echo "== Print createBoxInternal (around line ~130-260 from prior rg) =="
sed -n '120,320p' apps/api/src/box/services/box.service.ts | cat -n
echo
echo "== Print any runner-selection logic nearby (around where runnerId is assigned during create) =="
rg -n "runnerId\s*=|\.runnerId\s*=" apps/api/src/box/services/box.service.ts -g '*.ts'
rg -n "warmPool|WarmPool" apps/api/src/box/services/box.service.ts -g '*.ts'
echo
sed -n '320,520p' apps/api/src/box/services/box.service.ts | cat -nRepository: boxlite-ai/boxlite
Length of output: 28784
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== runner.service.ts: locate getRandomAvailableRunner =="
rg -n "getRandomAvailableRunner" apps/api/src/box/services/runner.service.ts
echo
echo "== Show implementation of getRandomAvailableRunner (likely around its definition) =="
# Print a generous window around the function definition
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("apps/api/src/box/services/runner.service.ts")
txt = p.read_text()
m = re.search(r'dasync\s+getRandomAvailableRunner|async\s+getRandomAvailableRunner', txt)
print("First occurrence index:", m.start() if m else None)
PY
# Fallback: just dump a mid-sized slice where it likely lives
sed -n '1,220p' apps/api/src/box/services/runner.service.ts | cat -n
sed -n '220,520p' apps/api/src/box/services/runner.service.ts | cat -n
sed -n '520,820p' apps/api/src/box/services/runner.service.ts | cat -n
echo
echo "== warm pool selection: search for apiVersion constraints in runner selection helpers =="
rg -n "getRandomAvailableRunner|getAvailableRunner|warmPool|WarmPool|apiVersion\s*===\s*'0'|apiVersion:\s*'0'|case '0'" apps/api/src/box/services -g '*.ts'
echo
echo "== Warm-pool related runner selection in box-warm-pool service (if present) =="
rg -n "getRandomAvailableRunner|runnerId|apiVersion" apps/api/src/box/services/box-warm-pool.service.ts -g '*.ts' || true
ls -la apps/api/src/box/services | sed -n '1,200p'Repository: boxlite-ai/boxlite
Length of output: 45510
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== box.service.ts: createForWarmPool (runner selection) =="
sed -n '100,220p' apps/api/src/box/services/box.service.ts | cat -n
echo
echo "== box.service.ts: createBoxInternal (runner selection) =="
sed -n '140,240p' apps/api/src/box/services/box.service.ts | cat -n
echo
echo "== runner.service.ts: findAvailableRunners (filters) =="
sed -n '60,140p' apps/api/src/box/services/runner.service.ts | cat -n
echo
echo "== warm-pool: any apiVersion filtering in box-warm-pool.service.ts =="
rg -n "apiVersion" apps/api/src/box/services/box-warm-pool.service.ts -g '*.ts' || true
sed -n '1,260p' apps/api/src/box/services/box-warm-pool.service.ts | cat -nRepository: boxlite-ai/boxlite
Length of output: 23599
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Box entity: default state / desiredState on construction =="
rg -n "class Box|constructor\(|desiredState|state:" apps/api/src/box/entities/box.entity.ts
sed -n '1,220p' apps/api/src/box/entities/box.entity.ts | cat -n
sed -n '220,420p' apps/api/src/box/entities/box.entity.ts | cat -n
echo
echo "== Find where BoxStartAction is triggered for desired STARTED =="
rg -n "BoxStartAction|desiredState.*STARTED|BoxDesiredState\.STARTED|BoxStateWaiterService|handleRunnerBoxUnknownStateOnDesiredStateStart" apps/api/src/box -g '*.ts'Repository: boxlite-ai/boxlite
Length of output: 14231
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,140p' apps/api/src/box/runner-adapter/runnerAdapter.ts | cat -nRepository: boxlite-ai/boxlite
Length of output: 3281
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== box.manager.ts: where BoxStartAction is called for pending boxes =="
rg -n "boxStartAction\.run|BoxStartAction|desiredState|pending" apps/api/src/box/managers/box.manager.ts
sed -n '1,220p' apps/api/src/box/managers/box.manager.ts | cat -n
sed -n '220,420p' apps/api/src/box/managers/box.manager.ts | cat -nRepository: boxlite-ai/boxlite
Length of output: 19863
Prevent V0 runners from being selected for box CREATE (current path will throw and put boxes in ERROR).
RunnerAdapterV0.createBox() unconditionally throws (createBox is not supported for V0 runners). New boxes are assigned a runner via runnerService.getRandomAvailableRunner() (no apiVersion filtering—only RunnerState.READY/unschedulable/draining/score), and BoxStartAction calls runnerAdapter.createBox() when the box is in BoxState.UNKNOWN with desired STARTED. If any V0 runner is READY, box creation can fail and transition the box to ERROR.
Fix by gating runner selection to apiVersion='2' for create/start-creation paths (or ensure the selected adapter supports createBox).
🤖 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/runner-adapter/runnerAdapter.v0.ts` around lines 147 - 150,
New boxes can be assigned a V0 runner that throws in
RunnerAdapterV0.createBox(), so update the runner-selection path to only choose
runners that support createBox (e.g., apiVersion === '2') to prevent boxes
moving to ERROR: modify the code that calls
runnerService.getRandomAvailableRunner() for create/start-creation flows (and
any BoxStartAction logic that invokes runnerAdapter.createBox() when BoxState is
UNKNOWN -> desired STARTED) to filter available runners by apiVersion === '2'
(or alternately add a capability check on the selected runner and retry/select
another runner if createBox is unsupported); ensure the check is applied where
runnerAdapter.createBox() is invoked so only adapters that implement createBox
are ever chosen.
| const payload = { | ||
| id: box.id, | ||
| userId: box.organizationId, | ||
| image: box.image, | ||
| osUser: box.osUser, | ||
| cpuQuota: box.cpu, | ||
| memoryQuota: box.mem, | ||
| storageQuota: box.disk, | ||
| env: box.env, | ||
| networkBlockAll: box.networkBlockAll, | ||
| networkAllowList: box.networkAllowList, | ||
| organizationId: box.organizationId, | ||
| regionId: box.region, | ||
| } |
There was a problem hiding this comment.
CREATE_BOX payload drops GPU quota, breaking resource contract.
box.gpu is set upstream, but createBox never sends a GPU field. That means GPU requests can be silently downgraded when the runner provisions from this payload.
Suggested fix
const payload = {
id: box.id,
userId: box.organizationId,
image: box.image,
osUser: box.osUser,
cpuQuota: box.cpu,
+ gpuQuota: box.gpu,
memoryQuota: box.mem,
storageQuota: box.disk,
env: box.env,
networkBlockAll: box.networkBlockAll,
networkAllowList: box.networkAllowList,🤖 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/runner-adapter/runnerAdapter.v2.ts` around lines 121 - 134,
The CREATE_BOX payload in runnerAdapter.v2.ts is missing the GPU quota and thus
drops box.gpu; update the payload object used to call createBox to include the
GPU field (e.g., add gpuQuota: box.gpu or the runner's expected key such as gpu:
box.gpu) alongside cpuQuota/memoryQuota/storageQuota, and ensure any related
type/interface for the payload (if present) is updated to include this GPU
property so the runner receives and honors the GPU request.
| `CREATE TABLE "box" ("id" character varying(12) NOT NULL, "organizationId" uuid NOT NULL, "name" character varying NOT NULL, "region" character varying NOT NULL, "runnerId" uuid, "prevRunnerId" uuid, "class" "public"."box_class_enum" NOT NULL DEFAULT 'small', "state" "public"."box_state_enum" NOT NULL DEFAULT 'unknown', "desiredState" "public"."box_desiredstate_enum" NOT NULL DEFAULT 'started', "osUser" character varying NOT NULL, "image" character varying NOT NULL, "errorReason" character varying, "recoverable" boolean NOT NULL DEFAULT false, "env" jsonb NOT NULL DEFAULT '{}', "public" boolean NOT NULL DEFAULT false, "networkBlockAll" boolean NOT NULL DEFAULT false, "networkAllowList" character varying, "labels" jsonb, "cpu" integer NOT NULL DEFAULT '2', "gpu" integer NOT NULL DEFAULT '0', "mem" integer NOT NULL DEFAULT '4', "disk" integer NOT NULL DEFAULT '10', "volumes" jsonb NOT NULL DEFAULT '[]', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "autoStopInterval" integer NOT NULL DEFAULT '15', "autoDeleteInterval" integer NOT NULL DEFAULT '-1', "pending" boolean NOT NULL DEFAULT false, "authToken" character varying NOT NULL, "daemonVersion" character varying, CONSTRAINT "box_organizationId_name_unique" UNIQUE ("organizationId", "name"), CONSTRAINT "box_id_pk" PRIMARY KEY ("id"))`, | ||
| ) |
There was a problem hiding this comment.
Enforce exact 12-char base62 box.id at the DB boundary.
Line 73 uses character varying(12), which only caps length; it still permits shorter/non-base62 IDs. Given the single 12-char base62 identity contract, add a DB check constraint so malformed IDs can’t persist.
Suggested migration patch
await queryRunner.query(
`CREATE TABLE "box" ("id" character varying(12) NOT NULL, "organizationId" uuid NOT NULL, "name" character varying NOT NULL, "region" character varying NOT NULL, "runnerId" uuid, "prevRunnerId" uuid, "class" "public"."box_class_enum" NOT NULL DEFAULT 'small', "state" "public"."box_state_enum" NOT NULL DEFAULT 'unknown', "desiredState" "public"."box_desiredstate_enum" NOT NULL DEFAULT 'started', "osUser" character varying NOT NULL, "image" character varying NOT NULL, "errorReason" character varying, "recoverable" boolean NOT NULL DEFAULT false, "env" jsonb NOT NULL DEFAULT '{}', "public" boolean NOT NULL DEFAULT false, "networkBlockAll" boolean NOT NULL DEFAULT false, "networkAllowList" character varying, "labels" jsonb, "cpu" integer NOT NULL DEFAULT '2', "gpu" integer NOT NULL DEFAULT '0', "mem" integer NOT NULL DEFAULT '4', "disk" integer NOT NULL DEFAULT '10', "volumes" jsonb NOT NULL DEFAULT '[]', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "autoStopInterval" integer NOT NULL DEFAULT '15', "autoDeleteInterval" integer NOT NULL DEFAULT '-1', "pending" boolean NOT NULL DEFAULT false, "authToken" character varying NOT NULL, "daemonVersion" character varying, CONSTRAINT "box_organizationId_name_unique" UNIQUE ("organizationId", "name"), CONSTRAINT "box_id_pk" PRIMARY KEY ("id"))`,
)
+ await queryRunner.query(
+ `ALTER TABLE "box" ADD CONSTRAINT "box_id_format_chk" CHECK ("id" ~ '^[0-9A-Za-z]{12}$')`,
+ )📝 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.
| `CREATE TABLE "box" ("id" character varying(12) NOT NULL, "organizationId" uuid NOT NULL, "name" character varying NOT NULL, "region" character varying NOT NULL, "runnerId" uuid, "prevRunnerId" uuid, "class" "public"."box_class_enum" NOT NULL DEFAULT 'small', "state" "public"."box_state_enum" NOT NULL DEFAULT 'unknown', "desiredState" "public"."box_desiredstate_enum" NOT NULL DEFAULT 'started', "osUser" character varying NOT NULL, "image" character varying NOT NULL, "errorReason" character varying, "recoverable" boolean NOT NULL DEFAULT false, "env" jsonb NOT NULL DEFAULT '{}', "public" boolean NOT NULL DEFAULT false, "networkBlockAll" boolean NOT NULL DEFAULT false, "networkAllowList" character varying, "labels" jsonb, "cpu" integer NOT NULL DEFAULT '2', "gpu" integer NOT NULL DEFAULT '0', "mem" integer NOT NULL DEFAULT '4', "disk" integer NOT NULL DEFAULT '10', "volumes" jsonb NOT NULL DEFAULT '[]', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "autoStopInterval" integer NOT NULL DEFAULT '15', "autoDeleteInterval" integer NOT NULL DEFAULT '-1', "pending" boolean NOT NULL DEFAULT false, "authToken" character varying NOT NULL, "daemonVersion" character varying, CONSTRAINT "box_organizationId_name_unique" UNIQUE ("organizationId", "name"), CONSTRAINT "box_id_pk" PRIMARY KEY ("id"))`, | |
| ) | |
| await queryRunner.query( | |
| `CREATE TABLE "box" ("id" character varying(12) NOT NULL, "organizationId" uuid NOT NULL, "name" character varying NOT NULL, "region" character varying NOT NULL, "runnerId" uuid, "prevRunnerId" uuid, "class" "public"."box_class_enum" NOT NULL DEFAULT 'small', "state" "public"."box_state_enum" NOT NULL DEFAULT 'unknown', "desiredState" "public"."box_desiredstate_enum" NOT NULL DEFAULT 'started', "osUser" character varying NOT NULL, "image" character varying NOT NULL, "errorReason" character varying, "recoverable" boolean NOT NULL DEFAULT false, "env" jsonb NOT NULL DEFAULT '{}', "public" boolean NOT NULL DEFAULT false, "networkBlockAll" boolean NOT NULL DEFAULT false, "networkAllowList" character varying, "labels" jsonb, "cpu" integer NOT NULL DEFAULT '2', "gpu" integer NOT NULL DEFAULT '0', "mem" integer NOT NULL DEFAULT '4', "disk" integer NOT NULL DEFAULT '10', "volumes" jsonb NOT NULL DEFAULT '[]', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "autoStopInterval" integer NOT NULL DEFAULT '15', "autoDeleteInterval" integer NOT NULL DEFAULT '-1', "pending" boolean NOT NULL DEFAULT false, "authToken" character varying NOT NULL, "daemonVersion" character varying, CONSTRAINT "box_organizationId_name_unique" UNIQUE ("organizationId", "name"), CONSTRAINT "box_id_pk" PRIMARY KEY ("id"))`, | |
| ) | |
| await queryRunner.query( | |
| `ALTER TABLE "box" ADD CONSTRAINT "box_id_format_chk" CHECK ("id" ~ '^[0-9A-Za-z]{12}$')`, | |
| ) |
🤖 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/1741087887225-migration.ts` around lines 73 - 74, The
CREATE TABLE for "box" currently uses character varying(12) for the id column
but allows shorter/non-base62 values; add a DB CHECK constraint to enforce
exactly 12 base62 chars by adding e.g. CONSTRAINT box_id_format_chk CHECK (id ~
'^[A-Za-z0-9]{12}$') into the CREATE TABLE statement (or ALTER TABLE ... ADD
CONSTRAINT for an existing table), and also ensure the migration's down path
removes that constraint (DROP CONSTRAINT box_id_format_chk) so rollbacks remain
consistent.
| // Box ids are 12-char base62 and case-sensitive; a case-insensitive match could resolve | ||
| // to the wrong box when two valid ids differ only by case. | ||
| const targetBoxId = boxId.trim() | ||
| for (const group of groups) { | ||
| const box = group.boxes.find((b) => b.id.toLowerCase() === targetBoxId || b.boxId?.toLowerCase() === targetBoxId) | ||
| const box = group.boxes.find((b) => b.id === targetBoxId) |
There was a problem hiding this comment.
Case-sensitive lookup now conflicts with existing lowercased caller input.
Line 225/Line 227 enforce exact-case id matching, but the current caller path (apps/dashboard/src/pages/Admin.tsx:86-94 in provided context) lowercases the pasted value before findBoxById. That causes legitimate IDs with uppercase characters to fail lookup.
Suggested fix (caller-side, outside this file)
- const trimmed = value.trim().toLowerCase()
+ const trimmed = value.trim()
if (!trimmed) return
const boxHit = findBoxById(groupBoxesByOwner(boxesQuery.data ?? []), trimmed)🤖 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/admin/adminHelpers.ts` around lines 223 - 227,
The lookup uses exact-case matching (const targetBoxId = boxId.trim();
group.boxes.find(b => b.id === targetBoxId)) but callers may lowercase pasted
ids; change the lookup to first attempt an exact match by id (keep targetBoxId
and the current strict compare), and if that returns undefined, perform a
fallback case-insensitive search (compare b.id.toLowerCase() ===
targetBoxId.toLowerCase()) so legitimate IDs with uppercase characters are still
found; update the code around targetBoxId and the group.boxes.find call
accordingly.
| **id** | **string** | Box ID | [default to undefined] | ||
| **boxId** | **string** | Public box ID shown to users | [optional] [default to undefined] | ||
| **organizationId** | **string** | Organization ID | [default to undefined] | ||
| **organizationId** | **string** | Public box ID shown to users | [optional] [default to undefined] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the organizationId description in the OpenAPI spec and other generated docs
# Check the OpenAPI spec for AdminBoxItem organizationId description
echo "=== Checking OpenAPI spec for AdminBoxItem.organizationId ==="
rg -A5 -B2 'organizationId' apps/api-client-go/api/openapi.yaml | head -50
# Check if other model docs have correct organizationId descriptions
echo ""
echo "=== Checking other model docs for organizationId descriptions ==="
rg -n 'organizationId.*\|' apps/libs/api-client/src/docs/Repository: boxlite-ai/boxlite
Length of output: 11513
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate AdminBoxItem schema in OpenAPI spec and inspect organizationId description
echo "=== Searching for AdminBoxItem in OpenAPI ==="
rg -n "AdminBoxItem" apps/api-client-go/api/openapi.yaml
echo ""
echo "=== Extracting AdminBoxItem schema block (best-effort) ==="
# Try to capture a reasonably-sized neighborhood around the schema
rg -n -C 20 "AdminBoxItem" apps/api-client-go/api/openapi.yaml | head -200
echo ""
echo "=== Looking specifically for organizationId within AdminBoxItem ==="
# Constrain search to nearby lines around AdminBoxItem occurrences
# (If schema is in components/schemas, this should still hit the right spot.)
rg -n -C 10 "AdminBoxItem" apps/api-client-go/api/openapi.yaml | rg -n "organizationId|public box|box id|boxId" || trueRepository: boxlite-ai/boxlite
Length of output: 4404
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Print the exact description for AdminBoxItem.organizationId from the OpenAPI schema
echo "=== OpenAPI: AdminBoxItem.organizationId description ==="
rg -n "AdminBoxItem:" apps/api-client-go/api/openapi.yaml
# Dump a tight window around the organizationId property within AdminBoxItem
# (The previous hit indicates organizationId starts around line ~13469)
sed -n '13460,13490p' apps/api-client-go/api/openapi.yaml
echo ""
echo "=== Docs: apps/libs/api-client/src/docs/AdminBoxItem.md (around line 9) ==="
nl -ba apps/libs/api-client/src/docs/AdminBoxItem.md | sed -n '1,30p'Repository: boxlite-ai/boxlite
Length of output: 1155
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Find definitions of AdminBoxItem / organizationId in backend code ==="
rg -n --hidden --no-ignore-vcs "AdminBoxItem" . | head -200
echo ""
echo "=== Find model/DTO fields named organizationId that relate to boxes ==="
rg -n --hidden --no-ignore-vcs "organizationId" apps src . | head -200
echo ""
echo "=== Search for admin overview boxes response mapping ==="
rg -n --hidden --no-ignore-vcs "adminListBoxes|/admin/overview/boxes|admin overview boxes" . | head -200
echo ""
echo "=== Look for any mention of boxId vs id/public box id ==="
rg -n --hidden --no-ignore-vcs "boxId|public box id|public box|Public box" . | head -200Repository: boxlite-ai/boxlite
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Inspect AdminBoxItemDto definition ==="
rg -n "export class AdminBoxItemDto|organizationId" apps/api/src/admin/dto/admin-overview.dto.ts
echo ""
echo "=== Show the AdminBoxItemDto class block around organizationId ==="
# Print a tight region around the class and the organizationId decorator/property
rg -n "export class AdminBoxItemDto" -n apps/api/src/admin/dto/admin-overview.dto.ts | head -5 | cut -d: -f1 > /tmp/class_line.txt
class_line=$(cat /tmp/class_line.txt)
start=$((class_line-30))
end=$((class_line+120))
sed -n "${start},${end}p" apps/api/src/admin/dto/admin-overview.dto.ts
echo ""
echo "=== Inspect overview.service.ts listBoxes mapping ==="
rg -n "listBoxes\\(|AdminBoxItemDto|organizationId|id\\b" apps/api/src/admin/services/overview.service.ts | head -80
echo ""
echo "=== Show listBoxes implementation region ==="
sed -n '1,220p' apps/api/src/admin/services/overview.service.ts
echo ""
echo "=== Inspect overview.controller.ts response and DTO usage ==="
sed -n '1,140p' apps/api/src/admin/controllers/overview.controller.tsRepository: boxlite-ai/boxlite
Length of output: 14225
Fix incorrect organizationId description in AdminBoxItem docs/OpenAPI
AdminBoxItemDto.organizationId is documented as “Public box ID shown to users”, but it represents the organization’s ID (the box public ID is id). The incorrect description is emitted into apps/api-client-go/api/openapi.yaml and apps/libs/api-client/src/docs/AdminBoxItem.md.
Root cause: apps/api/src/admin/dto/admin-overview.dto.ts defines two Swagger decorators for organizationId with conflicting descriptions; align to a single “Organization ID” description and regenerate docs/clients.
📝 Suggested fix
-**organizationId** | **string** | Public box ID shown to users | [optional] [default to undefined]
+**organizationId** | **string** | Organization ID | [optional] [default to undefined]📝 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.
| **organizationId** | **string** | Public box ID shown to users | [optional] [default to undefined] | |
| **organizationId** | **string** | Organization ID | [optional] [default to undefined] |
🤖 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/libs/api-client/src/docs/AdminBoxItem.md` at line 9, The
AdminBoxItemDto.organizationId field has conflicting Swagger descriptions in
admin-overview.dto.ts causing incorrect docs; open the DTO in
admin-overview.dto.ts, locate the decorators applied to
AdminBoxItemDto.organizationId and remove or update the duplicate Swagger
decorator so there is a single `@ApiProperty` (or `@ApiPropertyOptional`) whose
description reads "Organization ID" (not "Public box ID shown to users"), then
regenerate the OpenAPI artifacts and clients/docs (so
apps/api-client-go/api/openapi.yaml and
apps/libs/api-client/src/docs/AdminBoxItem.md are updated).
| createDto := dto.CreateBoxDTO{ | ||
| Id: boxId, | ||
| ArtifactRef: "alpine:latest", | ||
| Image: "alpine:latest", | ||
| OsUser: recoverDto.OsUser, |
There was a problem hiding this comment.
Recovery should preserve the box’s original image instead of forcing alpine:latest.
Line 97 hardcodes a mutable image/tag for all recoveries, which can boot a different environment than the box being recovered.
Suggested fix
func (c *Client) RecoverBox(ctx context.Context, boxId string, recoverDto dto.RecoverBoxDTO) error {
c.logger.Info("recover box", "box", boxId)
+
+ bx, err := c.getOrFetchBox(ctx, boxId)
+ if err != nil {
+ return fmt.Errorf("failed to get box for recover: %w", err)
+ }
+ info, err := bx.Info(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get box info for recover: %w", err)
+ }
if err := c.Destroy(ctx, boxId); err != nil {
c.logger.Warn("failed to destroy during recover", "error", err)
}
createDto := dto.CreateBoxDTO{
Id: boxId,
- Image: "alpine:latest",
+ Image: info.Image,
OsUser: recoverDto.OsUser,📝 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.
| createDto := dto.CreateBoxDTO{ | |
| Id: boxId, | |
| ArtifactRef: "alpine:latest", | |
| Image: "alpine:latest", | |
| OsUser: recoverDto.OsUser, | |
| func (c *Client) RecoverBox(ctx context.Context, boxId string, recoverDto dto.RecoverBoxDTO) error { | |
| c.logger.Info("recover box", "box", boxId) | |
| bx, err := c.getOrFetchBox(ctx, boxId) | |
| if err != nil { | |
| return fmt.Errorf("failed to get box for recover: %w", err) | |
| } | |
| info, err := bx.Info(ctx) | |
| if err != nil { | |
| return fmt.Errorf("failed to get box info for recover: %w", err) | |
| } | |
| if err := c.Destroy(ctx, boxId); err != nil { | |
| c.logger.Warn("failed to destroy during recover", "error", err) | |
| } | |
| createDto := dto.CreateBoxDTO{ | |
| Id: boxId, | |
| Image: info.Image, | |
| OsUser: recoverDto.OsUser, |
🤖 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/runner/pkg/boxlite/stubs.go` around lines 95 - 98, The code is
hardcoding "alpine:latest" when building dto.CreateBoxDTO (createDto) during
recovery; change it to use the original box image instead (e.g., set Image to
recoverDto.Image or the originalBox.Image/box.Image field that contains the
box's image tag) so the recovered box boots the same environment; update any
callers/tests that assumed the hardcoded value if needed.
|
Split up. Superseded by:
|
Summary
Backend image + identity refactor against
main. No dashboard cloudBox migration, no TS SDK package removal — those stay in #748.imageis a first-classBoxcolumn; the full OCI ref flows end-to-end (request → PG → CREATE_BOX payload →runtime.Create) with no curated-key⇄ref translation. A thin allowlist gate validates at the request boundary only. Runner contract field isimage. e2e adapted.boxIdinto one 12-char base62 id (the PK). The internal engine id is never exposed;boxId(the redundant duplicate of the cloud id) is dropped everywhere.Box/AdminBoxItem/WorkspaceloseboxId, gainimage.boxIdfixed in place (this is NOT the SDK removal): dashboard reads switchboxId→id(6 files);sdk-typescript'sBoxmirrors the new api-client shape (dropboxIdfield, addimage). The SDK package is untouched.Scope boundary (what's deferred to #748)
sdk-typescriptpackage, the dashboardcloudBoxmigration, the Playground/VNC retirement, and the image-picker UI.sdk-typescript/src/Box.ts(field mirror) so it keeps compiling after the regen — the package deletion is refactor(contracts): single-owner API contracts — cloud API is cloud-only, Box API owns box operations #748's job.Verification
sdk-typescriptbuild OK · dashboardtsc --noEmitclean · API jest 112/112 ·go vetclean (runner pkg) · engine unchanged vsmain.Deployment
Baseline migration edited in place (box PK =
varchar(12),imagecolumn added,boxIddropped) → requires a DB rebuild (agreed; nothing launched).Summary by CodeRabbit
Release Notes
New Features
Breaking Changes
Boxschema: removedboxIdfield, added requiredimagefield.Workspaceschema: removedboxId, madeimagerequired.AdminBoxItemschema:organizationIdis now optional.