Skip to content

feat(sdk): pre-signed sandbox file download/upload URLs#23

Open
MDzaja wants to merge 1 commit into
mainfrom
clients/pr-5099-download-link
Open

feat(sdk): pre-signed sandbox file download/upload URLs#23
MDzaja wants to merge 1 commit into
mainfrom
clients/pr-5099-download-link

Conversation

@MDzaja

@MDzaja MDzaja commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Original monorepo PR: daytonaio/daytona#5099

Summary

Client-side counterpart of the pre-signed sandbox file URL feature: SDK helpers for signing file
download/upload URLs and key rotation across all five SDKs, the regenerated API/toolbox clients,
and the vendored openapi specs.

Client changes included

  • SDKs: sdk-typescript, sdk-python, sdk-go, sdk-java, sdk-rubyfileUrlSigning
    utilities, Sandbox/FileSystem signing methods, and e2e tests.
  • Generated clients: api-client*, toolbox-api-client* — signing-key endpoints + UploadedFile
    model.
  • openapi-specs/api.json, openapi-specs/toolbox.json.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Add pre-signed sandbox file URLs with signing-key fetch/rotation and SDK helpers to generate download/upload links. Also update toolbox upload to accept multipart or raw bodies and return an UploadedFile, with regenerated api-client* and toolbox-api-client* plus vendored OpenAPI specs.

  • New Features

    • SDKs (sdk-typescript, sdk-python, sdk-go, sdk-java, sdk-ruby): file URL signing utilities and Sandbox/FileSystem helpers to create signed download/upload URLs; lightweight signing-key caching; e2e tests.
    • API clients (api-client*): add GET /sandbox/{sandboxId}/signing-key and rotate endpoints; preview API: signing-key getter; includes tests.
    • Toolbox clients (toolbox-api-client*): upload now accepts multipart file or raw body and returns UploadedFile.
  • Migration

    • Toolbox uploadFile now returns UploadedFile (name/path/type). Update callers expecting a generic map/object.
    • Ruby: toolbox_api.upload_file(path, file: io_or_file) replaces the positional file arg. Other SDKs now treat the file as optional (raw body supported).
    • Signed URLs default to 1h TTL; key rotation immediately invalidates existing links.

Written for commit eda6686. Summary will update on new commits.

Review in cubic

Signed-off-by: MDzaja <mirkodzaja0@gmail.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

30 issues found across 68 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="toolbox-api-client-go/model_uploaded_file.go">

<violation number="1" location="toolbox-api-client-go/model_uploaded_file.go:163">
P2: Required string fields allow JSON null and are coerced to empty strings. This bypasses required-field validation and can hide invalid API payloads.</violation>
</file>

<file name="openapi-specs/toolbox.json">

<violation number="1" location="openapi-specs/toolbox.json:2838">
P2: Spec advertises raw-body upload support but models only multipart upload. This creates incorrect generated client/docs behavior for upload-v2.</violation>
</file>

<file name="api-client-ruby/lib/daytona_api_client/api/preview_api.rb">

<violation number="1" location="api-client-ruby/lib/daytona_api_client/api/preview_api.rb:145">
P1: Do not log the returned signing key; redact `data` in this debug message to avoid leaking signed-URL secrets into application logs.</violation>
</file>

<file name="toolbox-api-client-python-async/daytona_toolbox_api_client_async/api/file_system_api.py">

<violation number="1" location="toolbox-api-client-python-async/daytona_toolbox_api_client_async/api/file_system_api.py:3079">
P2: `upload_file` promises raw request body uploads, but the generated method exposes only the multipart `file` field and never passes a body. Callers using `application/octet-stream` as documented will fail instead of uploading raw bytes.</violation>
</file>

<file name="toolbox-api-client/src/api/file-system-api.ts">

<violation number="1" location="toolbox-api-client/src/api/file-system-api.ts:496">
P1: Raw-body uploads are advertised but impossible through this client because `options.data` is overwritten with FormData when `file` is omitted.</violation>
</file>

<file name="sdk-go/pkg/daytona/e2e_test.go">

<violation number="1" location="sdk-go/pkg/daytona/e2e_test.go:919">
P3: HTTP calls use default client with no timeout. If the server hangs, the test goroutine blocks until the test binary's hard timeout kills it.</violation>
</file>

<file name="api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb">

<violation number="1" location="api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb:1399">
P1: Sensitive signing key is written to debug logs. Redact/remove response body logging for this endpoint.</violation>

<violation number="2" location="api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb:2561">
P1: Rotated signing key is exposed through debug response logging. Remove/redact `Data` from this log line.</violation>
</file>

<file name="toolbox-api-client-python/daytona_toolbox_api_client/api/file_system_api.py">

<violation number="1" location="toolbox-api-client-python/daytona_toolbox_api_client/api/file_system_api.py:3063">
P2: `upload_file` now allows `file=None`, but the serializer has no raw-body parameter path, so advertised non-multipart uploads are not actually supported. This can send empty uploads or fail when callers set `_content_type` to `application/octet-stream`.</violation>
</file>

<file name="sdk-typescript/src/utils/fileUrlSigning.ts">

<violation number="1" location="sdk-typescript/src/utils/fileUrlSigning.ts:6">
P2: New signing helper is Node-crypto-only and can break browser runtime for `downloadUrl`/`uploadUrl`. Add a browser-compatible HMAC path (Web Crypto) or explicitly gate this API with a clear runtime error.</violation>
</file>

<file name="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/api/FileSystemApi.java">

<violation number="1" location="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/api/FileSystemApi.java:1633">
P2: `_file` is now advertised as nullable, but a null upload builds an empty multipart request and fails client-side before the API call. Keep the file required for this overload or add a separate raw-body upload path.</violation>
</file>

<file name="toolbox-api-client-go/api_file_system.go">

<violation number="1" location="toolbox-api-client-go/api_file_system.go:178">
P2: UploadFile advertises raw-body uploads, but this client cannot provide a raw request payload. This makes the new contract unreachable from Go and can send empty invalid requests when no file is set.</violation>
</file>

<file name="sdk-python/src/daytona/_utils/file_url_signing.py">

<violation number="1" location="sdk-python/src/daytona/_utils/file_url_signing.py:27">
P2: `resolve_expires` accepts non-integer TTL values and can generate signed URLs with non-integer `expires` values. That can produce signatures the server rejects even though URL generation succeeded.</violation>

<violation number="2" location="sdk-python/src/daytona/_utils/file_url_signing.py:48">
P3: URL construction does not normalize a trailing slash in `toolbox_proxy_url`, which can emit double-slash paths. Some proxies/routers treat `//` paths differently and may fail routing.</violation>
</file>

<file name="sdk-java/src/test/java/io/daytona/sdk/E2ETest.java">

<violation number="1" location="sdk-java/src/test/java/io/daytona/sdk/E2ETest.java:569">
P2: HttpClient.newHttpClient() creates clients with no timeout — a hung server blocks the test suite indefinitely. Add connect + read timeouts.</violation>
</file>

<file name="sdk-java/src/main/java/io/daytona/sdk/Sandbox.java">

<violation number="1" location="sdk-java/src/main/java/io/daytona/sdk/Sandbox.java:359">
P2: Use the resolved toolbox proxy URL for signed file URLs. Otherwise objects created from DTOs with missing/empty toolboxProxyUrl can return invalid relative download/upload URLs even though the constructor already resolved a working proxy for toolbox APIs.</violation>
</file>

<file name="sdk-ruby/lib/daytona/utils/file_url_signing.rb">

<violation number="1" location="sdk-ruby/lib/daytona/utils/file_url_signing.rb:47">
P2: Normalize `toolbox_proxy_url` before appending `sandbox_id`; otherwise DTO values ending in `/` produce signed URLs with a doubled slash before the sandbox ID, which can miss strict proxy routes.</violation>
</file>

<file name="sdk-go/pkg/daytona/file_url_signing.go">

<violation number="1" location="sdk-go/pkg/daytona/file_url_signing.go:48">
P3: Unreachable error branch: `resolveExpires` never returns a non-nil error, so `if err != nil` is dead code. Remove the unused error return path to simplify and clarify control flow.</violation>

<violation number="2" location="sdk-go/pkg/daytona/file_url_signing.go:58">
P2: Signed file URL construction does not normalize trailing slash on toolboxProxyUrl, which can generate `//` in the path and break pre-signed URL routing.</violation>
</file>

<file name="sdk-go/pkg/daytona/sandbox.go">

<violation number="1" location="sdk-go/pkg/daytona/sandbox.go:427">
P3: Use `time.Since(...) <= 15*time.Second` instead of `.Seconds() <= 15` for idiomatic duration comparison, which avoids floating-point comparison and is more readable.</violation>

<violation number="2" location="sdk-go/pkg/daytona/sandbox.go:429">
P2: Thundering herd: when the cached key expires, concurrent goroutines all release the lock and fetch the key in parallel. Consider `singleflight` to coalesce concurrent fetches into one API call.</violation>
</file>

<file name="sdk-java/src/main/java/io/daytona/sdk/FileUrlSigning.java">

<violation number="1" location="sdk-java/src/main/java/io/daytona/sdk/FileUrlSigning.java:65">
P1: Missing toolbox proxy URL is silently turned into a relative signed URL. Sandboxes constructed from DTOs without toolboxProxyUrl can return `/<sandboxId>/files/...` instead of an absolute URL for unauthenticated HTTP clients.</violation>
</file>

<file name="sdk-python/tests/test_e2e.py">

<violation number="1" location="sdk-python/tests/test_e2e.py:936">
P3: Full signed URL is interpolated into assertion error messages — leaks a valid signed URL to CI logs on test failure.</violation>

<violation number="2" location="sdk-python/tests/test_e2e.py:940">
P2: Signed URL tests share implicit filesystem state (`SIGNED_URL_TEST_DIR` directory and files) without ordering enforcement — they break under `pytest-random-order` or any non-definition collection order.</violation>
</file>

<file name="sdk-python/tests/test_async_e2e.py">

<violation number="1" location="sdk-python/tests/test_async_e2e.py:197">
P2: Signed URL tests have implicit ordering dependency without enforcement. Running individual tests via `-k` fails with opaque errors because `test_async_signed_url_setup` hasn't created the directory/file.</violation>
</file>

<file name="sdk-typescript/src/Sandbox.ts">

<violation number="1" location="sdk-typescript/src/Sandbox.ts:795">
P3: Inconsistent trailing-slash handling: constructor normalizes `toolboxProxyUrl` for the axios base URL, but `downloadUrl`/`uploadUrl` pass it raw to `buildSignedFileUrl` which unconditionally inserts a `/` separator. If the API ever returns a URL with a trailing slash, signed URLs will have `//` in the path while regular API calls won't.</violation>
</file>

<file name="sdk-python/src/daytona/_async/sandbox.py">

<violation number="1" location="sdk-python/src/daytona/_async/sandbox.py:275">
P3: Signing-key TTL uses wall-clock time instead of a monotonic clock. Clock adjustments can break refresh timing and prolong stale-key usage.</violation>

<violation number="2" location="sdk-python/src/daytona/_async/sandbox.py:277">
P2: Signing-key cache has a race between fetch and rotate. Concurrent calls can overwrite a rotated key with an older key and generate invalid URLs.</violation>
</file>

<file name="sdk-python/src/daytona/_sync/sandbox.py">

<violation number="1" location="sdk-python/src/daytona/_sync/sandbox.py:289">
P3: Magic number `15` (signing key cache TTL in seconds) should be a named constant for maintainability and consistency with the docstrings that reference it.</violation>
</file>

<file name="toolbox-api-client-ruby/lib/daytona_toolbox_api_client/api/file_system_api.rb">

<violation number="1" location="toolbox-api-client-ruby/lib/daytona_toolbox_api_client/api/file_system_api.rb:768">
P1: Ruby `upload_file` became incompatible with existing positional `file` calls; passing a File as second argument now causes runtime errors because the method expects an options hash.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic


data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
if @api_client.config.debugging
@api_client.config.logger.debug "API called: PreviewApi#get_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Do not log the returned signing key; redact data in this debug message to avoid leaking signed-URL secrets into application logs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-client-ruby/lib/daytona_api_client/api/preview_api.rb, line 145:

<comment>Do not log the returned signing key; redact `data` in this debug message to avoid leaking signed-URL secrets into application logs.</comment>

<file context>
@@ -86,6 +86,67 @@ def get_sandbox_id_from_signed_preview_url_token_with_http_info(signed_preview_t
+
+      data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
+      if @api_client.config.debugging
+        @api_client.config.logger.debug "API called: PreviewApi#get_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
+      end
+      return data, status_code, headers
</file context>
Suggested change
@api_client.config.logger.debug "API called: PreviewApi#get_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
@api_client.config.logger.debug "API called: PreviewApi#get_signing_key\nData: [REDACTED]\nStatus code: #{status_code}\nHeaders: #{headers}"
Fix with cubic

* @throws {RequiredError}
*/
uploadFile: async (path: string, file: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
uploadFile: async (path: string, file?: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Raw-body uploads are advertised but impossible through this client because options.data is overwritten with FormData when file is omitted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At toolbox-api-client/src/api/file-system-api.ts, line 496:

<comment>Raw-body uploads are advertised but impossible through this client because `options.data` is overwritten with FormData when `file` is omitted.</comment>

<file context>
@@ -484,19 +486,17 @@ export const FileSystemApiAxiosParamCreator = function (configuration?: Configur
          * @throws {RequiredError}
          */
-        uploadFile: async (path: string, file: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
+        uploadFile: async (path: string, file?: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'path' is not null or undefined
             assertParamExists('uploadFile', 'path', path)
</file context>
Fix with cubic


data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
if @api_client.config.debugging
@api_client.config.logger.debug "API called: SandboxApi#rotate_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Rotated signing key is exposed through debug response logging. Remove/redact Data from this log line.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb, line 2561:

<comment>Rotated signing key is exposed through debug response logging. Remove/redact `Data` from this log line.</comment>

<file context>
@@ -2435,6 +2499,70 @@ def revoke_ssh_access_with_http_info(sandbox_id_or_name, opts = {})
+
+      data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
+      if @api_client.config.debugging
+        @api_client.config.logger.debug "API called: SandboxApi#rotate_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
+      end
+      return data, status_code, headers
</file context>
Suggested change
@api_client.config.logger.debug "API called: SandboxApi#rotate_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
@api_client.config.logger.debug "API called: SandboxApi#rotate_signing_key\nStatus code: #{status_code}\nHeaders: #{headers}"
Fix with cubic


data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
if @api_client.config.debugging
@api_client.config.logger.debug "API called: SandboxApi#get_sandbox_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Sensitive signing key is written to debug logs. Redact/remove response body logging for this endpoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb, line 1399:

<comment>Sensitive signing key is written to debug logs. Redact/remove response body logging for this endpoint.</comment>

<file context>
@@ -1337,6 +1337,70 @@ def get_sandbox_parent_with_http_info(sandbox_id_or_name, opts = {})
+
+      data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
+      if @api_client.config.debugging
+        @api_client.config.logger.debug "API called: SandboxApi#get_sandbox_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
+      end
+      return data, status_code, headers
</file context>
Suggested change
@api_client.config.logger.debug "API called: SandboxApi#get_sandbox_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
@api_client.config.logger.debug "API called: SandboxApi#get_sandbox_signing_key\nStatus code: #{status_code}\nHeaders: #{headers}"
Fix with cubic

String encodedExpires = URLEncoder.encode(String.valueOf(expires), StandardCharsets.UTF_8);
String encodedSignature = URLEncoder.encode(signature, StandardCharsets.UTF_8);

return trimTrailingSlash(toolboxProxyUrl) + "/" + sandboxId + operationPath

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Missing toolbox proxy URL is silently turned into a relative signed URL. Sandboxes constructed from DTOs without toolboxProxyUrl can return /<sandboxId>/files/... instead of an absolute URL for unauthenticated HTTP clients.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-java/src/main/java/io/daytona/sdk/FileUrlSigning.java, line 65:

<comment>Missing toolbox proxy URL is silently turned into a relative signed URL. Sandboxes constructed from DTOs without toolboxProxyUrl can return `/<sandboxId>/files/...` instead of an absolute URL for unauthenticated HTTP clients.</comment>

<file context>
@@ -0,0 +1,81 @@
+        String encodedExpires = URLEncoder.encode(String.valueOf(expires), StandardCharsets.UTF_8);
+        String encodedSignature = URLEncoder.encode(signature, StandardCharsets.UTF_8);
+
+        return trimTrailingSlash(toolboxProxyUrl) + "/" + sandboxId + operationPath
+                + "?path=" + encodedPath
+                + "&expires=" + encodedExpires
</file context>
Fix with cubic


func (s *Sandbox) ensureSigningKey(ctx context.Context) (string, error) {
s.signingKeyMu.Lock()
if s.signingKey != "" && time.Since(s.signingKeyFetchedAt).Seconds() <= 15 {

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Use time.Since(...) <= 15*time.Second instead of .Seconds() <= 15 for idiomatic duration comparison, which avoids floating-point comparison and is more readable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-go/pkg/daytona/sandbox.go, line 427:

<comment>Use `time.Since(...) <= 15*time.Second` instead of `.Seconds() <= 15` for idiomatic duration comparison, which avoids floating-point comparison and is more readable.</comment>

<file context>
@@ -415,6 +422,29 @@ func (s *Sandbox) doRefreshData(ctx context.Context) error {
 
+func (s *Sandbox) ensureSigningKey(ctx context.Context) (string, error) {
+	s.signingKeyMu.Lock()
+	if s.signingKey != "" && time.Since(s.signingKeyFetchedAt).Seconds() <= 15 {
+		key := s.signingKey
+		s.signingKeyMu.Unlock()
</file context>
Suggested change
if s.signingKey != "" && time.Since(s.signingKeyFetchedAt).Seconds() <= 15 {
if s.signingKey != "" && time.Since(s.signingKeyFetchedAt) <= 15*time.Second {
Fix with cubic

def test_download_url_returns_signed_url(sandbox):
url = sandbox.download_url(f"{SIGNED_URL_TEST_DIR}/download-test.txt")
assert isinstance(url, str) and len(url) > 0, "download_url should return a non-empty string"
assert "signature=" in url, f"URL should contain signature param: {url}"

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Full signed URL is interpolated into assertion error messages — leaks a valid signed URL to CI logs on test failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-python/tests/test_e2e.py, line 936:

<comment>Full signed URL is interpolated into assertion error messages — leaks a valid signed URL to CI logs on test failure.</comment>

<file context>
@@ -913,6 +914,53 @@ def test_sandbox_remains_usable_after_archive_cycle(sandbox):
+def test_download_url_returns_signed_url(sandbox):
+    url = sandbox.download_url(f"{SIGNED_URL_TEST_DIR}/download-test.txt")
+    assert isinstance(url, str) and len(url) > 0, "download_url should return a non-empty string"
+    assert "signature=" in url, f"URL should contain signature param: {url}"
+    assert "expires=" in url, f"URL should contain expires param: {url}"
+
</file context>
Suggested change
assert "signature=" in url, f"URL should contain signature param: {url}"
assert "signature=" in url, "download_url should return a signed URL"
Fix with cubic

@WithInstrumentation()
public async downloadUrl(path: string, ttlSeconds?: number): Promise<string> {
return buildSignedFileUrl(
this.toolboxProxyUrl,

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Inconsistent trailing-slash handling: constructor normalizes toolboxProxyUrl for the axios base URL, but downloadUrl/uploadUrl pass it raw to buildSignedFileUrl which unconditionally inserts a / separator. If the API ever returns a URL with a trailing slash, signed URLs will have // in the path while regular API calls won't.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-typescript/src/Sandbox.ts, line 795:

<comment>Inconsistent trailing-slash handling: constructor normalizes `toolboxProxyUrl` for the axios base URL, but `downloadUrl`/`uploadUrl` pass it raw to `buildSignedFileUrl` which unconditionally inserts a `/` separator. If the API ever returns a URL with a trailing slash, signed URLs will have `//` in the path while regular API calls won't.</comment>

<file context>
@@ -768,6 +771,78 @@ export class Sandbox {
+  @WithInstrumentation()
+  public async downloadUrl(path: string, ttlSeconds?: number): Promise<string> {
+    return buildSignedFileUrl(
+      this.toolboxProxyUrl,
+      this.id,
+      '/files/download',
</file context>
Fix with cubic


async def _ensure_signing_key(self) -> str:
key = self._signing_key
if key is None or (time.time() - self._signing_key_fetched_at) > 15:

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Signing-key TTL uses wall-clock time instead of a monotonic clock. Clock adjustments can break refresh timing and prolong stale-key usage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-python/src/daytona/_async/sandbox.py, line 275:

<comment>Signing-key TTL uses wall-clock time instead of a monotonic clock. Clock adjustments can break refresh timing and prolong stale-key usage.</comment>

<file context>
@@ -267,6 +270,100 @@ async def set_labels(self, labels: dict[str, str]) -> dict[str, str]:
 
+    async def _ensure_signing_key(self) -> str:
+        key = self._signing_key
+        if key is None or (time.time() - self._signing_key_fetched_at) > 15:
+            key = await self._sandbox_api.get_sandbox_signing_key(self.id)
+            self._signing_key = key
</file context>
Fix with cubic


def _ensure_signing_key(self) -> str:
key = self._signing_key
if key is None or (time.time() - self._signing_key_fetched_at) > 15:

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Magic number 15 (signing key cache TTL in seconds) should be a named constant for maintainability and consistency with the docstrings that reference it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-python/src/daytona/_sync/sandbox.py, line 289:

<comment>Magic number `15` (signing key cache TTL in seconds) should be a named constant for maintainability and consistency with the docstrings that reference it.</comment>

<file context>
@@ -281,6 +284,88 @@ def set_labels(self, labels: dict[str, str]) -> dict[str, str]:
 
+    def _ensure_signing_key(self) -> str:
+        key = self._signing_key
+        if key is None or (time.time() - self._signing_key_fetched_at) > 15:
+            key = self._sandbox_api.get_sandbox_signing_key(self.id)
+            self._signing_key = key
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant