Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdks/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Always:
- Keep package-local validation fast before widening to multi-language verification.
- Match public behavior across languages unless a documented platform constraint prevents it.
- Keep wire-format units and public SDK units separate. Public SDK interfaces should expose time durations as language-native duration types where available (`timedelta`, `Duration`) or otherwise as explicitly second-based fields such as `timeoutSeconds`.
- For Kotlin SDK public APIs intended for Java interoperability, do not expose Kotlin value classes such as `kotlin.time.Duration`; they are JVM-name-mangled and can be inaccessible from Java. Prefer `java.time.Duration` or explicit primitive wire units at the public boundary, with deprecated Kotlin-friendly overloads when needed for migration.

Ask first:

Expand Down
2 changes: 1 addition & 1 deletion sdks/sandbox/kotlin/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ org.gradle.parallel=true

# Project metadata
project.group=com.alibaba.opensandbox
project.version=1.0.11
project.version=1.0.12
project.description=A Kotlin SDK for Open Sandbox API
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class ConnectionConfig private constructor(
private const val ENV_API_KEY = "OPEN_SANDBOX_API_KEY"
private const val ENV_DOMAIN = "OPEN_SANDBOX_DOMAIN"

private const val DEFAULT_USER_AGENT = "OpenSandbox-Kotlin-SDK/1.0.11"
private const val DEFAULT_USER_AGENT = "OpenSandbox-Kotlin-SDK/1.0.12"
private const val API_VERSION = "v1"

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

package com.alibaba.opensandbox.sandbox.domain.models.execd.executions

import kotlin.time.Duration
import java.time.Duration
Comment thread
ninan-nn marked this conversation as resolved.
Comment thread
ninan-nn marked this conversation as resolved.
import kotlin.time.toJavaDuration

/**
* Parameters for command execution.
Expand Down Expand Up @@ -75,11 +76,19 @@ class RunCommandRequest private constructor(
* Maximum execution time; server will terminate the command when reached.
* If omitted, the server will not enforce any timeout.
*/
fun timeout(timeout: Duration?): Builder {
fun timeout(timeout: Duration): Builder {
this.timeout = timeout
return this
}

@Deprecated(
message = "Use java.time.Duration instead.",
replaceWith = ReplaceWith("timeout(timeout.toJavaDuration())", "kotlin.time.toJavaDuration"),
)
fun timeout(timeout: kotlin.time.Duration): Builder {
return timeout(timeout.toJavaDuration())
}

fun uid(uid: Int?): Builder {
require(uid == null || uid >= 0) { "Uid must be >= 0" }
this.uid = uid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

package com.alibaba.opensandbox.sandbox.domain.models.execd.executions

import kotlin.time.Duration
import java.time.Duration
Comment thread
ninan-nn marked this conversation as resolved.
import kotlin.time.toJavaDuration

/**
* Request to run a command in an existing bash session.
Expand Down Expand Up @@ -54,11 +55,19 @@ class RunInSessionRequest private constructor(
return this
}

fun timeout(timeout: Duration?): Builder {
fun timeout(timeout: Duration): Builder {
this.timeout = timeout
return this
}

@Deprecated(
message = "Use java.time.Duration instead.",
replaceWith = ReplaceWith("timeout(timeout.toJavaDuration())", "kotlin.time.toJavaDuration"),
)
fun timeout(timeout: kotlin.time.Duration): Builder {
return timeout(timeout.toJavaDuration())
}

fun handlers(handlers: ExecutionHandlers?): Builder {
this.handlers = handlers
return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.CommandSta
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.Execution
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunCommandRequest
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunInSessionRequest
import kotlin.time.Duration
import java.time.Duration
import kotlin.time.toJavaDuration

/**
* Command execution operations for sandbox environments.
Expand Down Expand Up @@ -115,14 +116,31 @@ interface Commands {
workingDirectory: String? = null,
timeout: Duration? = null,
): Execution {
return runInSession(
sessionId,
val builder =
RunInSessionRequest.builder()
.command(command)
.workingDirectory(workingDirectory)
.timeout(timeout)
.build(),
)
if (timeout != null) {
builder.timeout(timeout)
}
return runInSession(sessionId, builder.build())
}

@Deprecated(
message = "Use java.time.Duration instead.",
replaceWith =
ReplaceWith(
"runInSession(sessionId, command, workingDirectory, timeout.toJavaDuration())",
"kotlin.time.toJavaDuration",
),
)
fun runInSession(
sessionId: String,
command: String,
workingDirectory: String? = null,
timeout: kotlin.time.Duration,
Comment thread
ninan-nn marked this conversation as resolved.
): Execution {
return runInSession(sessionId, command, workingDirectory, timeout.toJavaDuration())
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter

import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.CommandStatus
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunCommandRequest
import java.time.Duration
import com.alibaba.opensandbox.sandbox.api.models.execd.CommandStatusResponse as ApiCommandStatusResponse
import com.alibaba.opensandbox.sandbox.api.models.execd.RunCommandRequest as ApiRunCommandRequest

Expand All @@ -27,7 +28,7 @@ object ExecutionConverter {
command = command,
background = background,
cwd = workingDirectory,
timeout = timeout?.inWholeMilliseconds,
timeout = timeout?.toCommandTimeoutMillis(),
uid = uid,
gid = gid,
envs = envs,
Expand All @@ -46,3 +47,12 @@ object ExecutionConverter {
)
}
}

internal fun Duration.toCommandTimeoutMillis(): Long {
require(!isNegative) { "Timeout must be non-negative, got: $this" }
return try {
toMillis()
} catch (e: ArithmeticException) {
throw IllegalArgumentException("Timeout is too large to represent in milliseconds: $this", e)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.Executi
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.ExecutionEventDispatcher
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.jsonParser
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.parseSandboxError
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.toCommandTimeoutMillis
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.toSandboxException
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
Expand Down Expand Up @@ -196,7 +197,7 @@ internal class CommandsAdapter(
RunInSessionRequestApi(
command = request.command,
cwd = request.workingDirectory,
timeout = request.timeout?.inWholeMilliseconds,
timeout = request.timeout?.toCommandTimeoutMillis(),
)
val runUrl =
execdBaseUrl
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.opensandbox.sandbox.domain.models.execd.executions

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.Duration
import kotlin.time.Duration.Companion.seconds

class RunCommandRequestTest {
@Test
fun `builder accepts java duration for timeout`() {
val request =
RunCommandRequest.builder()
.command("echo hi")
.timeout(Duration.ofSeconds(5))
.build()

assertEquals(Duration.ofSeconds(5), request.timeout)
}

@Suppress("DEPRECATION")
@Test
fun `builder accepts deprecated kotlin duration for timeout`() {
val request =
RunCommandRequest.builder()
.command("echo hi")
.timeout(5.seconds)
.build()

assertEquals(Duration.ofSeconds(5), request.timeout)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.opensandbox.sandbox.domain.models.execd.executions

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.Duration
import kotlin.time.Duration.Companion.seconds

class RunInSessionRequestTest {
@Test
fun `builder accepts java duration for timeout`() {
val request =
RunInSessionRequest.builder()
.command("echo hi")
.timeout(Duration.ofSeconds(5))
.build()

assertEquals(Duration.ofSeconds(5), request.timeout)
}

@Suppress("DEPRECATION")
@Test
fun `builder accepts deprecated kotlin duration for timeout`() {
val request =
RunInSessionRequest.builder()
.command("echo hi")
.timeout(5.seconds)
.build()

assertEquals(Duration.ofSeconds(5), request.timeout)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.ExecutionH
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunCommandRequest
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunInSessionRequest
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.SandboxEndpoint
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.toCommandTimeoutMillis
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.intOrNull
Expand All @@ -39,9 +40,9 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.time.Duration
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds

class CommandsAdapterTest {
// CommandsAdapter unit tests
Expand Down Expand Up @@ -340,7 +341,7 @@ data: {"type":"execution_complete","execution_time":100,"timestamp":167253120100
RunInSessionRequest.builder()
.command("echo Hello")
.workingDirectory("/workspace")
.timeout(5.seconds)
.timeout(Duration.ofSeconds(5))
.handlers(handlers)
.build(),
)
Expand All @@ -359,6 +360,15 @@ data: {"type":"execution_complete","execution_time":100,"timestamp":167253120100
assertEquals(5000L, requestBodyJson["timeout"]?.jsonPrimitive?.content?.toLong())
}

@Test
fun `command timeout conversion should reject durations too large for milliseconds`() {
val exception =
assertThrows(IllegalArgumentException::class.java) {
Duration.ofSeconds(Long.MAX_VALUE).toCommandTimeoutMillis()
}
assertTrue(exception.message!!.contains("too large to represent in milliseconds"))
}

@Test
fun `runInSession should infer non-zero exit code from command error event`() {
val initEvent = """data: {"type":"init","text":"cmd-123","timestamp":1672531200000}"""
Expand Down
Loading