diff --git a/apps/api/src/box/dto/box.dto.ts b/apps/api/src/box/dto/box.dto.ts index aedbe1001..09b45cbf3 100644 --- a/apps/api/src/box/dto/box.dto.ts +++ b/apps/api/src/box/dto/box.dto.ts @@ -7,7 +7,7 @@ import { ApiProperty, ApiPropertyOptional, ApiSchema } from '@nestjs/swagger' import { BoxState } from '../enums/box-state.enum' import { IsEnum, IsOptional } from 'class-validator' -import { Box } from '../entities/box.entity' +import { Box, BoxLaunchConfig } from '../entities/box.entity' import { BoxDesiredState } from '../enums/box-desired-state.enum' import { BoxClass } from '../enums/box-class.enum' @@ -203,6 +203,16 @@ export class BoxDto { @IsOptional() volumes?: BoxVolume[] + @ApiPropertyOptional({ + description: 'Launch configuration for the box main process.', + type: 'object', + additionalProperties: true, + required: false, + }) + @IsOptional() + // Return the persisted launch configuration for later foreground attach and recovery flows. + launchConfig?: BoxLaunchConfig + @ApiPropertyOptional({ description: 'The creation timestamp of the box', example: '2024-10-01T12:00:00Z', @@ -270,6 +280,7 @@ export class BoxDto { networkAllowList: box.networkAllowList, labels: box.labels, volumes: box.volumes, + launchConfig: box.launchConfig, state: this.getBoxState(box), desiredState: box.desiredState, errorReason: box.errorReason, diff --git a/apps/api/src/box/dto/create-box.dto.ts b/apps/api/src/box/dto/create-box.dto.ts index be0d7ea93..781db31f7 100644 --- a/apps/api/src/box/dto/create-box.dto.ts +++ b/apps/api/src/box/dto/create-box.dto.ts @@ -8,6 +8,7 @@ import { IsEnum, IsObject, IsOptional, IsString, IsNumber, IsBoolean, IsArray, I import { ApiPropertyOptional, ApiSchema } from '@nestjs/swagger' import { BoxClass } from '../enums/box-class.enum' import { BoxVolume } from './box.dto' +import { BoxLaunchConfig } from '../entities/box.entity' @ApiSchema({ name: 'CreateBox' }) export class CreateBoxDto { @@ -170,4 +171,14 @@ export class CreateBoxDto { @IsOptional() @IsArray() volumes?: BoxVolume[] + + @ApiPropertyOptional({ + description: 'Launch configuration for the box main process. Used by remote foreground run.', + type: 'object', + additionalProperties: true, + }) + @IsOptional() + @IsObject() + // The REST boundary validates and maps these fields; the service layer only persists them. + launchConfig?: BoxLaunchConfig } diff --git a/apps/api/src/box/entities/box.entity.ts b/apps/api/src/box/entities/box.entity.ts index a4eb59fbf..fa2a2e2f8 100644 --- a/apps/api/src/box/entities/box.entity.ts +++ b/apps/api/src/box/entities/box.entity.ts @@ -18,6 +18,29 @@ import { DEFAULT_AUTO_RESUME, } from '../constants/box-lifecycle.constants' +/** + * Launch configuration for the Box main process. + * + * A remote foreground run cannot rely on transient create-request parameters. After an API + * or Runner restart, the control plane still needs these fields to resume the create-attach-start flow. + */ +export interface BoxLaunchConfig { + /** Overrides the image's default entrypoint. */ + entrypoint?: string[] + /** Command and arguments passed to the entrypoint. */ + cmd?: string[] + /** Working directory used when starting the main process. */ + workingDir?: string + /** Whether to allocate a terminal for the main process. */ + tty?: boolean + /** Whether the client requests detachment from the main process. */ + detach?: boolean + /** Whether to attach the output stream before starting the main process. */ + foreground?: boolean + /** Whether to delete the Box after the foreground process exits, implementing remote `run --rm`. */ + autoDeleteAfterExit?: boolean +} + @Entity('box') @Unique(['organizationId', 'name']) @Index('box_state_idx', ['state']) @@ -138,6 +161,13 @@ export class Box { }) volumes: BoxVolume[] = [] + @Column({ + type: 'jsonb', + nullable: true, + }) + // Persist with the Box so foreground launch semantics survive control-plane restarts. + launchConfig?: BoxLaunchConfig + @CreateDateColumn({ type: 'timestamp with time zone', }) diff --git a/apps/api/src/box/managers/box-actions/box-start.action.spec.ts b/apps/api/src/box/managers/box-actions/box-start.action.spec.ts index e0b112820..9c3dfeae7 100644 --- a/apps/api/src/box/managers/box-actions/box-start.action.spec.ts +++ b/apps/api/src/box/managers/box-actions/box-start.action.spec.ts @@ -168,11 +168,64 @@ describe('BoxStartAction.handleRunnerBoxUnknownStateOnDesiredStateStart', () => const result = await (action as BoxAction).run(box, lockCode) - expect(createBox).toHaveBeenCalledWith(box, expect.any(Object)) + expect(createBox).toHaveBeenCalledWith(box, expect.any(Object), false) expect(result).toBe(SYNC_AGAIN) expect(updatedFields.some((u) => u.state === BoxState.CREATING)).toBe(true) }) + it('creates a foreground box without starting it on a v0 runner', async () => { + const runnerId = 'runner-boot-foreground' + + const box = new Box('region-1', 'foreground-box') + box.runnerId = runnerId + box.image = 'boxlite/base' + box.state = BoxState.UNKNOWN + box.desiredState = BoxDesiredState.STARTED + box.pending = true + box.launchConfig = { + cmd: ['-lc', 'echo foreground-ok; exit 7'], + detach: false, + foreground: true, + autoDeleteAfterExit: true, + } + + const runner = { id: runnerId, state: RunnerState.READY, apiVersion: '0' } as Runner + const runnerService = { findOneOrFail: jest.fn(async () => runner) } + + const createBox = jest.fn(async () => undefined) + const runnerAdapterFactory = { create: jest.fn(async () => ({ createBox }) as any) } + + const lockCode = new LockCode('lock-boot-foreground') + const updatedFields: Partial[] = [] + const boxRepository = { + update: jest.fn(async (_id: string, opts: { updateData: Partial }) => { + updatedFields.push(opts.updateData) + return box + }), + } + const redisLockProvider = { getCode: jest.fn(async () => lockCode) } + const organizationService = { findOne: jest.fn(async () => ({ boxMetadata: {} })) } + + const action = new BoxStartAction( + runnerService as any, + runnerAdapterFactory as any, + boxRepository as any, + organizationService as any, + {} as any, + redisLockProvider as any, + {} as any, + ) + + const result = await (action as BoxAction).run(box, lockCode) + + expect(createBox).toHaveBeenCalledWith(box, expect.any(Object), true) + expect(result).not.toBe(SYNC_AGAIN) + expect(updatedFields).toContainEqual({ + state: BoxState.STOPPED, + desiredState: BoxDesiredState.STOPPED, + }) + }) + it('moves an unknown box with no image to ERROR without calling createBox', async () => { const runnerId = 'runner-boot-2' diff --git a/apps/api/src/box/managers/box-actions/box-start.action.ts b/apps/api/src/box/managers/box-actions/box-start.action.ts index 414de0c97..4e5a42a05 100644 --- a/apps/api/src/box/managers/box-actions/box-start.action.ts +++ b/apps/api/src/box/managers/box-actions/box-start.action.ts @@ -8,6 +8,7 @@ import { Injectable, Logger } from '@nestjs/common' import { BoxRepository } from '../../repositories/box.repository' import { Box } from '../../entities/box.entity' import { BoxState } from '../../enums/box-state.enum' +import { BoxDesiredState } from '../../enums/box-desired-state.enum' import { DONT_SYNC_AGAIN, BoxAction, SYNC_AGAIN, SyncState } from './box.action' import { RunnerState } from '../../enums/runner-state.enum' import { RunnerService } from '../../services/runner.service' @@ -77,7 +78,26 @@ export class BoxStartAction extends BoxAction { } const runnerAdapter = await this.runnerAdapterFactory.create(runner) - await runnerAdapter.createBox(box, metadata) + // Prepare the VM and main-process configuration first, delaying process start so fast commands + // cannot exit before the client attaches. + const skipStart = box.launchConfig?.foreground === true + await runnerAdapter.createBox(box, metadata, skipStart) + + if (skipStart && runner.apiVersion === '0') { + // V0 creation is synchronous; when skipStart returns successfully, the Box exists but is not running. + // Update desiredState as well so the state reconciler does not immediately start it. + await this.updateBoxState( + box, + BoxState.STOPPED, + lockCode, + undefined, + undefined, + undefined, + undefined, + { desiredState: BoxDesiredState.STOPPED }, + ) + return DONT_SYNC_AGAIN + } await this.updateBoxState(box, BoxState.CREATING, lockCode) return SYNC_AGAIN diff --git a/apps/api/src/box/managers/box-actions/box.action.ts b/apps/api/src/box/managers/box-actions/box.action.ts index 89dcba875..e46034e78 100644 --- a/apps/api/src/box/managers/box-actions/box.action.ts +++ b/apps/api/src/box/managers/box-actions/box.action.ts @@ -38,6 +38,7 @@ export abstract class BoxAction { errorReason?: string, daemonVersion?: string, recoverable?: boolean, + extraUpdateData?: Partial, ) { // check if the lock code is still valid const lockKey = getStateChangeLockKey(box.id) @@ -64,6 +65,8 @@ export abstract class BoxAction { } const updateData: Partial = { + // Some state transitions must atomically update related fields such as desiredState. + ...extraUpdateData, state, } diff --git a/apps/api/src/box/runner-adapter/runnerAdapter.ts b/apps/api/src/box/runner-adapter/runnerAdapter.ts index 1d1d023f0..7a832b33a 100644 --- a/apps/api/src/box/runner-adapter/runnerAdapter.ts +++ b/apps/api/src/box/runner-adapter/runnerAdapter.ts @@ -46,12 +46,19 @@ export interface RunnerAdapter { runnerInfo(signal?: AbortSignal): Promise boxInfo(boxId: string): Promise - createBox(box: Box, metadata?: { [key: string]: string }): Promise + /** + * Creates a Box. When skipStart=true, it prepares the runtime without starting the main process, + * allowing the remote foreground flow to attach the output stream before explicitly starting it. + */ + createBox( + box: Box, + metadata?: { [key: string]: string }, + skipStart?: boolean, + ): Promise startBox( boxId: string, authToken: string, metadata?: { [key: string]: string }, - skipStart?: boolean, ): Promise stopBox(boxId: string, force?: boolean): Promise destroyBox(boxId: string): Promise diff --git a/apps/api/src/box/runner-adapter/runnerAdapter.v0.ts b/apps/api/src/box/runner-adapter/runnerAdapter.v0.ts index dd6e55585..8ce7065b6 100644 --- a/apps/api/src/box/runner-adapter/runnerAdapter.v0.ts +++ b/apps/api/src/box/runner-adapter/runnerAdapter.v0.ts @@ -249,7 +249,11 @@ export class RunnerAdapterV0 implements RunnerAdapter { } } - async createBox(box: Box, metadata?: { [key: string]: string }): Promise { + async createBox( + box: Box, + metadata?: { [key: string]: string }, + skipStart?: boolean, + ): Promise { const response = await this.boxApiClient.create({ id: box.id, image: box.image ?? '', @@ -259,12 +263,20 @@ export class RunnerAdapterV0 implements RunnerAdapter { memoryQuota: box.mem, storageQuota: box.disk, env: box.env, + // The synchronous V0 API also needs the full configuration to preserve launch semantics + // during mixed-version deployments. + entrypoint: box.launchConfig?.entrypoint, + cmd: box.launchConfig?.cmd, + workingDir: box.launchConfig?.workingDir, + tty: box.launchConfig?.tty, + detach: box.launchConfig?.detach, networkBlockAll: box.networkBlockAll, networkAllowList: box.networkAllowList, metadata, authToken: box.authToken, organizationId: box.organizationId, regionId: box.region, + skipStart, }) if (!response?.data?.daemonVersion) { diff --git a/apps/api/src/box/runner-adapter/runnerAdapter.v2.ts b/apps/api/src/box/runner-adapter/runnerAdapter.v2.ts index ba4eee098..dd8482ef9 100644 --- a/apps/api/src/box/runner-adapter/runnerAdapter.v2.ts +++ b/apps/api/src/box/runner-adapter/runnerAdapter.v2.ts @@ -114,7 +114,11 @@ export class RunnerAdapterV2 implements RunnerAdapter { } } - async createBox(box: Box, metadata?: { [key: string]: string }): Promise { + async createBox( + box: Box, + metadata?: { [key: string]: string }, + skipStart?: boolean, + ): Promise { if (!box.image) { throw new Error(`Box ${box.id} has no image; cannot create on runner`) } @@ -128,6 +132,12 @@ export class RunnerAdapterV2 implements RunnerAdapter { memoryQuota: box.mem, storageQuota: box.disk, env: box.env, + // Store launch configuration in the async job payload so task replay survives Runner restarts. + entrypoint: box.launchConfig?.entrypoint, + cmd: box.launchConfig?.cmd, + workingDir: box.launchConfig?.workingDir, + tty: box.launchConfig?.tty, + detach: box.launchConfig?.detach, volumes: box.volumes?.map((volume) => ({ volumeId: volume.volumeId, mountPath: volume.mountPath, @@ -137,6 +147,7 @@ export class RunnerAdapterV2 implements RunnerAdapter { networkAllowList: box.networkAllowList, metadata, authToken: box.authToken, + skipStart, organizationId: box.organizationId, regionId: box.region, } diff --git a/apps/api/src/box/services/box.service.ts b/apps/api/src/box/services/box.service.ts index 675873b4e..f206cc4fd 100644 --- a/apps/api/src/box/services/box.service.ts +++ b/apps/api/src/box/services/box.service.ts @@ -255,6 +255,8 @@ export class BoxService { if (createBoxDto.volumes !== undefined) { box.volumes = this.resolveVolumes(createBoxDto.volumes) } + // Persist launch parameters with the Box instead of keeping them only in the initial Runner request. + box.launchConfig = createBoxDto.launchConfig box.runnerId = runner.id box.pending = true diff --git a/apps/api/src/box/services/job-state-handler.service.spec.ts b/apps/api/src/box/services/job-state-handler.service.spec.ts index 66748e32a..1d88b9901 100644 --- a/apps/api/src/box/services/job-state-handler.service.spec.ts +++ b/apps/api/src/box/services/job-state-handler.service.spec.ts @@ -52,3 +52,41 @@ describe('JobStateHandlerService RESIZE_BOX completion', () => { }) }) }) + +describe('JobStateHandlerService CREATE_BOX completion', () => { + it('keeps a foreground skip-start box stopped after a completed V2 create job', async () => { + const box = { + id: 'box-foreground', + state: BoxState.CREATING, + desiredState: BoxDesiredState.STARTED, + } + const boxRepository = { + findOne: jest.fn().mockResolvedValue(box), + update: jest.fn().mockResolvedValue(undefined), + } as any + const redisLockProvider = { + unlock: jest.fn().mockResolvedValue(undefined), + } as any + const service = new JobStateHandlerService(boxRepository, redisLockProvider) + const job = new Job({ + id: 'job-create-foreground', + type: JobType.CREATE_BOX, + status: JobStatus.COMPLETED, + runnerId: 'runner-1', + resourceType: ResourceType.BOX, + resourceId: box.id, + payload: JSON.stringify({ skipStart: true }), + }) + + await service.handleJobCompletion(job) + + expect(boxRepository.update).toHaveBeenCalledWith(box.id, { + updateData: { + state: BoxState.STOPPED, + desiredState: BoxDesiredState.STOPPED, + errorReason: null, + }, + entity: box, + }) + }) +}) diff --git a/apps/api/src/box/services/job-state-handler.service.ts b/apps/api/src/box/services/job-state-handler.service.ts index 8ae333836..cda6bf49d 100644 --- a/apps/api/src/box/services/job-state-handler.service.ts +++ b/apps/api/src/box/services/job-state-handler.service.ts @@ -102,8 +102,19 @@ export class JobStateHandlerService { const updateData: Partial = {} if (job.status === JobStatus.COMPLETED) { - this.logger.debug(`CREATE_BOX job ${job.id} completed successfully, marking box ${boxId} as STARTED`) - updateData.state = BoxState.STARTED + const payload = job.getPayload<{ skipStart?: boolean }>() ?? {} + // CREATE_BOX completion only means the runtime exists; a skipStart job has not started its main process. + const skippedStart = payload.skipStart === true + this.logger.debug( + `CREATE_BOX job ${job.id} completed successfully, marking box ${boxId} as ${ + skippedStart ? 'STOPPED' : 'STARTED' + }`, + ) + updateData.state = skippedStart ? BoxState.STOPPED : BoxState.STARTED + if (skippedStart) { + // Prevent the state reconciler from starting the main process before the client attaches its output stream. + updateData.desiredState = BoxDesiredState.STOPPED + } updateData.errorReason = null const metadata = job.getResultMetadata() if (metadata?.daemonVersion && typeof metadata.daemonVersion === 'string') { diff --git a/apps/api/src/boxlite-rest/boxlite-box.controller.ts b/apps/api/src/boxlite-rest/boxlite-box.controller.ts index e50184b44..46a89b9c9 100644 --- a/apps/api/src/boxlite-rest/boxlite-box.controller.ts +++ b/apps/api/src/boxlite-rest/boxlite-box.controller.ts @@ -75,6 +75,7 @@ export class BoxliteBoxController { working_dir: req.body?.working_dir, entrypoint: req.body?.entrypoint, cmd: req.body?.cmd, + tty: req.body?.tty, detach: req.body?.detach, auto_pause: req.body?.auto_pause, auto_delete: req.body?.auto_delete, @@ -90,6 +91,11 @@ export class BoxliteBoxController { const createBoxDto = createBoxToCreateBox(dto) let box = await this.boxService.create(createBoxDto, organization) + if (dto.detach === false) { + // Foreground mode creates a stopped Box first; a later flow starts it after attaching the output stream. + // Waiting for STARTED here would deadlock the attach-before-start sequence. + return boxToBoxResponse(box) + } if (box.state !== BoxState.STARTED) { box = await this.boxStateWaiter.waitForStarted(box.id, organization.id, 30) } diff --git a/apps/api/src/boxlite-rest/dto/create-box.dto.spec.ts b/apps/api/src/boxlite-rest/dto/create-box.dto.spec.ts index 962547cb3..4a014b325 100644 --- a/apps/api/src/boxlite-rest/dto/create-box.dto.spec.ts +++ b/apps/api/src/boxlite-rest/dto/create-box.dto.spec.ts @@ -71,6 +71,38 @@ describe('CreateBoxDto lifecycle policy', () => { }) }) +describe('CreateBoxDto launch config validation', () => { + it('accepts launch argv, working directory, tty, and detach fields', async () => { + const errors = await validate( + plainToInstance(CreateBoxDto, { + image: 'alpine:3.23', + entrypoint: ['/bin/sh'], + cmd: ['-lc', 'echo foreground-ok'], + working_dir: '/workspace', + tty: true, + detach: false, + }), + ) + + expect(errors).toHaveLength(0) + }) + + it('rejects non-string launch argv entries and non-boolean tty', async () => { + const errors = await validate( + plainToInstance(CreateBoxDto, { + image: 'alpine:3.23', + entrypoint: ['/bin/sh', 42], + cmd: ['-lc', false], + tty: 'true', + }), + ) + + expect(errors.find((error) => error.property === 'entrypoint')?.constraints).toHaveProperty('isString') + expect(errors.find((error) => error.property === 'cmd')?.constraints).toHaveProperty('isString') + expect(errors.find((error) => error.property === 'tty')?.constraints).toHaveProperty('isBoolean') + }) +}) + describe('CreateBoxDto network validation', () => { it('accepts supported allow_net entry types', async () => { const errors = await validate( diff --git a/apps/api/src/boxlite-rest/dto/create-box.dto.ts b/apps/api/src/boxlite-rest/dto/create-box.dto.ts index 9e0310371..0515d23fb 100644 --- a/apps/api/src/boxlite-rest/dto/create-box.dto.ts +++ b/apps/api/src/boxlite-rest/dto/create-box.dto.ts @@ -72,6 +72,7 @@ export class CreateBoxDto { @Min(1) disk_size_gb?: number + // These fields define how the main process starts and are validated before the control plane persists them. @IsOptional() @IsString() working_dir?: string @@ -82,16 +83,22 @@ export class CreateBoxDto { @IsOptional() @IsArray() + @IsString({ each: true }) entrypoint?: string[] @IsOptional() @IsArray() + @IsString({ each: true }) cmd?: string[] @IsOptional() @IsString() user?: string + @IsOptional() + @IsBoolean() + tty?: boolean + @IsOptional() @IsBoolean() detach?: boolean diff --git a/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts b/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts index fd0867021..8de126688 100644 --- a/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts +++ b/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts @@ -19,6 +19,34 @@ describe('BoxLite lifecycle policy mapper', () => { expect(mapped.autoResume).toBe(false) }) + it('maps foreground launch settings into the persisted control-plane DTO', () => { + const mapped = createBoxToCreateBox({ + image: 'sandbaseai-hermes', + entrypoint: ['/bin/sh'], + cmd: ['-lc', 'echo foreground-ok; exit 7'], + working_dir: '/workspace', + tty: true, + detach: false, + auto_delete: 1, + }) + + expect(mapped.launchConfig).toEqual({ + entrypoint: ['/bin/sh'], + cmd: ['-lc', 'echo foreground-ok; exit 7'], + workingDir: '/workspace', + tty: true, + detach: false, + foreground: true, + autoDeleteAfterExit: true, + }) + }) + + it('does not persist an empty launch config for ordinary creates', () => { + const mapped = createBoxToCreateBox({ image: 'alpine:3.23' }) + + expect(mapped.launchConfig).toBeUndefined() + }) + it('returns the effective second-based policy', () => { const response = boxToBoxResponse({ id: 'box-1', diff --git a/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts b/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts index f74a91e78..7c194a3b9 100644 --- a/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts +++ b/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts @@ -45,6 +45,26 @@ export function createBoxToCreateBox(dto: RestCreateBoxDto, target?: string): Cr createDto.autoPause = dto.auto_pause createDto.autoDelete = dto.auto_delete createDto.autoResume = dto.auto_resume + // Persist launch configuration only when explicitly supplied to preserve existing create behavior. + const hasLaunchConfig = + dto.entrypoint !== undefined || + dto.cmd !== undefined || + dto.working_dir !== undefined || + dto.tty !== undefined || + dto.detach !== undefined + if (hasLaunchConfig) { + createDto.launchConfig = { + entrypoint: dto.entrypoint, + cmd: dto.cmd, + workingDir: dto.working_dir, + tty: dto.tty, + detach: dto.detach, + // detach=false means the CLI expects local-like streaming output and the main process exit code. + foreground: dto.detach === false, + // Record cleanup intent; the lifecycle flow performs deletion after the foreground process exits. + autoDeleteAfterExit: dto.detach === false && dto.auto_delete !== undefined && dto.auto_delete > 0, + } + } if (dto.network) { const allowNet = dto.network.allow_net?.map((entry) => entry.trim()).filter(Boolean) createDto.networkBlockAll = dto.network.mode === 'disabled' diff --git a/apps/go.work.sum b/apps/go.work.sum index 8ddadcfd2..b84b510a6 100644 --- a/apps/go.work.sum +++ b/apps/go.work.sum @@ -43,10 +43,14 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= @@ -77,26 +81,48 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= @@ -108,6 +134,7 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -125,6 +152,8 @@ github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDP github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= @@ -138,18 +167,30 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/lyft/protoc-gen-star/v2 v2.0.4 h1:JDlNKttNIRd68AAIychs0AqEpO8/I/WYi01OQ7Raw6Q= +github.com/lyft/protoc-gen-star/v2 v2.0.4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/otiai10/gosseract/v2 v2.4.1 h1:G8AyBpXEeSlcq8TI85LH/pM5SXk8Djy2GEXisgyblRw= github.com/otiai10/gosseract/v2 v2.4.1/go.mod h1:1gNWP4Hgr2o7yqWfs6r5bZxAatjOIdqWxJLWsTsembk= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= @@ -163,6 +204,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= @@ -170,10 +213,13 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= @@ -193,6 +239,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= @@ -208,9 +256,12 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5w go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= @@ -220,6 +271,8 @@ golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -235,7 +288,9 @@ golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3c golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= @@ -249,6 +304,7 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= @@ -269,6 +325,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go. google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401001100-f93e5f3e9f0f/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds= google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a h1:UIpYSuWdWHSzjwcAFRLjKcPXFZVVLXGEM23W+NWqipw= @@ -277,6 +335,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/apps/libs/runner-api-client/src/models/create-box-dto.ts b/apps/libs/runner-api-client/src/models/create-box-dto.ts index 2558b7e2c..e0eab71b6 100644 --- a/apps/libs/runner-api-client/src/models/create-box-dto.ts +++ b/apps/libs/runner-api-client/src/models/create-box-dto.ts @@ -23,6 +23,8 @@ import type { RegistryDTO } from './registry-dto'; export interface CreateBoxDTO { 'authToken'?: string; 'cpuQuota'?: number; + 'cmd'?: Array; + 'detach'?: boolean; 'entrypoint'?: Array; 'env'?: { [key: string]: string; }; 'fromVolumeId'?: string; @@ -43,6 +45,7 @@ export interface CreateBoxDTO { 'skipStart'?: boolean; 'image': string; 'storageQuota'?: number; + 'tty'?: boolean; 'volumes'?: Array; + 'workingDir'?: string; } - diff --git a/apps/runner/pkg/api/dto/box.go b/apps/runner/pkg/api/dto/box.go index d3bb57430..459699a5d 100644 --- a/apps/runner/pkg/api/dto/box.go +++ b/apps/runner/pkg/api/dto/box.go @@ -5,17 +5,26 @@ package dto type CreateBoxDTO struct { - Id string `json:"id" validate:"required"` - FromVolumeId string `json:"fromVolumeId,omitempty"` - Image string `json:"image" validate:"required"` - OsUser string `json:"osUser" validate:"required"` - CpuQuota int64 `json:"cpuQuota" validate:"min=1"` - GpuQuota int64 `json:"gpuQuota" validate:"min=0"` - MemoryQuota int64 `json:"memoryQuota" validate:"min=1"` - StorageQuota int64 `json:"storageQuota" validate:"min=1"` - Env map[string]string `json:"env,omitempty"` - Registry *RegistryDTO `json:"registry,omitempty"` - Entrypoint []string `json:"entrypoint,omitempty"` + Id string `json:"id" validate:"required"` + FromVolumeId string `json:"fromVolumeId,omitempty"` + Image string `json:"image" validate:"required"` + OsUser string `json:"osUser" validate:"required"` + CpuQuota int64 `json:"cpuQuota" validate:"min=1"` + GpuQuota int64 `json:"gpuQuota" validate:"min=0"` + MemoryQuota int64 `json:"memoryQuota" validate:"min=1"` + StorageQuota int64 `json:"storageQuota" validate:"min=1"` + Env map[string]string `json:"env,omitempty"` + Registry *RegistryDTO `json:"registry,omitempty"` + Entrypoint []string `json:"entrypoint,omitempty"` + // The following fields describe how the main process starts and are persisted by the control plane. + // Overrides the image CMD with the main process command or default arguments. + Cmd []string `json:"cmd,omitempty"` + // Sets the main process working directory inside the Box. + WorkingDir *string `json:"workingDir,omitempty"` + // Requests a pseudo-terminal for interactive commands. + TTY *bool `json:"tty,omitempty"` + // Controls whether the Box survives after its creating parent process exits. + Detach *bool `json:"detach,omitempty"` Volumes []VolumeDTO `json:"volumes,omitempty"` NetworkBlockAll *bool `json:"networkBlockAll,omitempty"` NetworkAllowList *string `json:"networkAllowList,omitempty"` diff --git a/apps/runner/pkg/boxlite/client.go b/apps/runner/pkg/boxlite/client.go index eed31e44d..87621f7bb 100644 --- a/apps/runner/pkg/boxlite/client.go +++ b/apps/runner/pkg/boxlite/client.go @@ -247,6 +247,20 @@ func (c *Client) Create(ctx context.Context, boxDto dto.CreateBoxDTO) (string, s if len(boxDto.Entrypoint) > 0 { opts = append(opts, boxlite.WithEntrypoint(boxDto.Entrypoint...)) } + // Convert control-plane main-process settings into Go SDK options; unspecified fields retain image defaults. + if len(boxDto.Cmd) > 0 { + opts = append(opts, boxlite.WithCmd(boxDto.Cmd...)) + } + if boxDto.WorkingDir != nil && *boxDto.WorkingDir != "" { + opts = append(opts, boxlite.WithWorkDir(*boxDto.WorkingDir)) + } + if boxDto.Detach != nil { + opts = append(opts, boxlite.WithDetach(*boxDto.Detach)) + } + if boxDto.TTY != nil && *boxDto.TTY { + // The Go SDK does not yet expose a create-time TTY option, so log the capability gap explicitly. + c.logger.WarnContext(ctx, "main-process TTY was requested but the Go SDK does not expose a create-time TTY option yet", "box", boxDto.Id) + } volumeMounts, err := c.getVolumeMounts(ctx, boxDto.Volumes) if err != nil {