Skip to content

feat(dify-plugin): port external PRs#28

Open
mislavivanda wants to merge 9 commits into
mainfrom
port/dify-external-prs
Open

feat(dify-plugin): port external PRs#28
mislavivanda wants to merge 9 commits into
mainfrom
port/dify-external-prs

Conversation

@mislavivanda

@mislavivanda mislavivanda commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary by cubic

Ports the remaining external Dify plugin work and adds file, git, lifecycle, and background-service tools for Daytona sandboxes. Improves reliability and performance with a cached client, safer file transfers, broader port validation, and richer outputs.

  • New Features

    • File ops: list, read, write, glob search, and content search inside a sandbox (list_files, read_file, write_file, search_files, find_in_files).
    • Git and lifecycle: clone repos, list sandboxes, and start/stop/archive (git_clone, list_sandboxes, manage_sandbox).
    • Background services: start long-running processes and fetch logs (start_service, get_service_logs).
    • Tool upgrades: run_command uses process.exec with optional cwd, env_vars, and timeout; run_code emits chart PNGs as image blobs and adds chart metadata.
  • Performance & Reliability

    • Cache the Daytona client per credentials and map generic SDK errors to clear messages when fetching sandboxes.
    • Enforce a 100 MB limit for uploads/downloads to prevent OOM.
    • Accept ports 1–65535 in get_preview_url; treat blank numeric fields in create_sandbox as unset.
    • Align new tools with current SDK behaviors and register them in provider/daytona.yaml.

Written for commit 260bafe. Summary will update on new commits.

Review in cubic

mislavivanda and others added 9 commits July 13, 2026 16:20
…rm fields

Ports the still-relevant parts of PR #1 from the archived daytona/dify-plugin. The SDK-compat list() call and SDK version pin were already handled by the monorepo cubic-review fixes, so this ports the remainder: widen get_preview_url to ports 1-65535 (was 3000-9999), and treat blank auto_stop_interval/cpu/memory/disk form values as unset so Dify's empty-string inputs never reach to_int(). Original work by @hjpinheiro (daytona/dify-plugin#1).

Co-authored-by: hpiclaranet <hpiclaranet@users.noreply.github.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
Ports PR #2 from the archived daytona/dify-plugin: a shared MAX_FILE_SIZE (100 MB) in _client.py, checked against file.blob before upload and against get_file_info().size (pre-download) plus content length (post-download) — preventing a large file from OOM-crashing the plugin daemon. Original work by @hjpinheiro (daytona/dify-plugin#2).

Co-authored-by: hpiclaranet <hpiclaranet@users.noreply.github.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
… error handling

Ports PR #3 from the archived daytona/dify-plugin: build_client() now caches one Daytona instance per credential hash (the plugin daemon is long-lived, so this avoids re-creating an HTTP session on every tool call), and get_sandbox() also maps a generic DaytonaError to a clear ValueError. Omitted the PR's unused module logger to keep the file lint-clean. Original work by @hjpinheiro (daytona/dify-plugin#3).

Co-authored-by: hpiclaranet <hpiclaranet@users.noreply.github.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
Ports PR #4 from the archived daytona/dify-plugin: replaces the session-based create_session/execute_session_command/delete_session dance with a single sandbox.process.exec() call, adding optional working directory, environment variables (JSON), and timeout. Uses the monorepo's to_int() for the timeout param and guards a missing command. Original work by @hjpinheiro (daytona/dify-plugin#4).

Co-authored-by: hpiclaranet <hpiclaranet@users.noreply.github.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
Ports PR #5 from the archived daytona/dify-plugin: when Python code produces charts (matplotlib/etc.), the Daytona SDK returns them as base64 PNGs in response.artifacts.charts. These were previously discarded; now each is decoded and yielded as an image/png blob message so users see rendered charts in Dify, with chart metadata added to the JSON result. Original work by @hjpinheiro (daytona/dify-plugin#5).

Co-authored-by: hpiclaranet <hpiclaranet@users.noreply.github.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
Ports PR #6 from the archived daytona/dify-plugin: adds list_files, read_file, write_file, search_files and find_in_files tools (with yaml manifests) for working with files inside a sandbox, and registers them in provider/daytona.yaml. Original work by @hjpinheiro (daytona/dify-plugin#6).

Co-authored-by: hpiclaranet <hpiclaranet@users.noreply.github.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
Ports PR #7 from the archived daytona/dify-plugin: adds git_clone (clone a repo into a sandbox), list_sandboxes (list with state filter), and manage_sandbox (start/stop/archive), plus yaml manifests, registered in provider/daytona.yaml. Original work by @hjpinheiro (daytona/dify-plugin#7).

Co-authored-by: hpiclaranet <hpiclaranet@users.noreply.github.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
…ervice_logs)

Ports PR #8 from the archived daytona/dify-plugin: start_service runs a long-lived command in an async session, and get_service_logs retrieves its output — enabling background processes (web servers, watchers) in a sandbox. Registered in provider/daytona.yaml. Original work by @hjpinheiro (daytona/dify-plugin#8).

Co-authored-by: hpiclaranet <hpiclaranet@users.noreply.github.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
Verifying the ported PRs against the Daytona SDK reference surfaced drift vs the pinned 0.193+ (the fork targeted ~0.187):

- start_service: SessionExecuteRequest field is run_async, not var_async.
- find_in_files: fs.find_files returns a flat list[Match] (.file/.line/.content), not nested r.matches/r.file.path.
- search_files: fs.search_files returns SearchFilesResponse (.files), not a list (len()/JSON on it would fail).
- list_sandboxes: remove unused SandboxState import (lint error).
- run_code: chart PNGs are on chart.png (chart.elements are data points, not images) — the old path delivered zero charts; also serialize the ChartType enum.

Signed-off-by: Mislav Ivanda <mislavivanda454@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.

35 issues found across 30 files

You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

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="apps/dify-plugin/_client.py">

<violation number="1" location="apps/dify-plugin/_client.py:10">
P2: A long-lived, multi-user plugin daemon will retain every Daytona client and its HTTP connection resources indefinitely because `_client_cache` has no eviction or cleanup. This grows with credential churn and tenant count; a bounded cache with an eviction callback that closes the `Daytona` client would preserve session reuse without turning credentials/clients into process-lifetime state.</violation>

<violation number="2" location="apps/dify-plugin/_client.py:14">
P1: Distinct `api_key`/`api_url` pairs can resolve to the same cache entry, causing a tool call to reuse another credential set's authenticated Daytona client. Encode field boundaries unambiguously before hashing, such as with length-prefixed values.</violation>

<violation number="3" location="apps/dify-plugin/_client.py:30">
P2: Concurrent first calls for the same credentials can create and use multiple Daytona clients, defeating the one-client-per-credential invariant and potentially abandoning the overwritten session. Protect the check/create/store sequence with a lock.</violation>
</file>

<file name="apps/dify-plugin/tools/get_service_logs.py">

<violation number="1" location="apps/dify-plugin/tools/get_service_logs.py:36">
P2: Log retrieval is unbounded for the long-running processes this tool targets: it loads the command's complete history and then sends it once as text and again as `stdout`/`stderr` JSON. A noisy service can therefore make each poll consume an ever-growing amount of memory and exceed Dify tool-message limits. Consider adding a bounded tail/maximum-output contract and avoid duplicating the full log in both messages.</violation>

<violation number="2" location="apps/dify-plugin/tools/get_service_logs.py:41">
P3: Fallback rendering changes log formatting by adding a leading newline for stderr-only output and potentially doubling an existing stdout newline. Add a separator only when both streams are non-empty and stdout does not already end with a newline.</violation>

<violation number="3" location="apps/dify-plugin/tools/get_service_logs.py:43">
P3: Completed silent commands are reported as though they are still pending because no command state is checked before emitting “no output yet.” A neutral “(no output)” avoids misleading users.</violation>
</file>

<file name="apps/dify-plugin/tools/run_command.py">

<violation number="1" location="apps/dify-plugin/tools/run_command.py:25">
P2: Negative timeout values are accepted by this tool and forwarded to the Daytona SDK, where they can become an invalid command/HTTP timeout rather than a meaningful execution limit. Since the Dify `number` schema does not impose a minimum, validating `timeout >= 0` here would give callers a deterministic error (while retaining `0`/empty as no timeout if that is the intended contract).</violation>

<violation number="2" location="apps/dify-plugin/tools/run_command.py:38">
P1: Run Command no longer honors its public response contract: callers expecting `stdout` and `stderr` now receive an undocumented combined `output` field instead. Preserve separate stream execution/results and return the documented keys to avoid breaking existing Dify workflows.</violation>
</file>

<file name="apps/dify-plugin/tools/download_file.py">

<violation number="1" location="apps/dify-plugin/tools/download_file.py:33">
P1: An oversized or concurrently growing sandbox file can still exhaust the plugin worker's memory because the size limit is checked only after `download_file()` has materialized all bytes. Consider downloading through a bounded streaming/temp-file path and aborting once `MAX_FILE_SIZE` is exceeded, while retaining the metadata precheck as an optimization.</violation>
</file>

<file name="apps/dify-plugin/tools/write_file.py">

<violation number="1" location="apps/dify-plugin/tools/write_file.py:28">
P2: `write_file` can upload content larger than the shared 100 MB limit that `upload_file` now enforces. Large text inputs therefore bypass the plugin's transfer guard and can consume excessive memory/network resources. The tool should import `MAX_FILE_SIZE` and reject `len(encoded) > MAX_FILE_SIZE` before `sandbox.fs.upload_file`.</violation>

<violation number="2" location="apps/dify-plugin/tools/write_file.py:30">
P3: Writing an empty file performs a second remote lookup after the upload, so a metadata error can make a successful overwrite appear failed. `size_bytes = len(encoded)` handles empty and non-empty content without that extra failure point.</violation>
</file>

<file name="apps/dify-plugin/tools/run_command.yaml">

<violation number="1" location="apps/dify-plugin/tools/run_command.yaml:15">
P3: The public README still promises separate `stdout` and `stderr` fields, while this changed tool contract now returns only combined `output`. Updating the Run Command feature and Tool Reference alongside this schema change would prevent workflow authors from building against fields the tool no longer emits.</violation>

<violation number="2" location="apps/dify-plugin/tools/run_command.yaml:73">
P2: Leaving `timeout` empty still uses Daytona's default command timeout, so long-running commands can terminate despite the new UI text promising no limit. Describe the default accurately and direct users to `0` when they need indefinite execution.</violation>
</file>

<file name="apps/dify-plugin/tools/list_sandboxes.yaml">

<violation number="1" location="apps/dify-plugin/tools/list_sandboxes.yaml:12">
P3: Chinese and Japanese users will see English tool and parameter descriptions despite selecting `zh_Hans` or `ja_JP`. Translating these locale entries would make the newly exposed tool consistent with its declared localization metadata.</violation>
</file>

<file name="apps/dify-plugin/tools/upload_file.py">

<violation number="1" location="apps/dify-plugin/tools/upload_file.py:27">
P2: Oversized Dify files are fully downloaded into plugin memory before this limit rejects them, so the 100 MB guard does not prevent the memory pressure it is meant to bound. Checking `file.size` before accessing `file.blob`, while retaining the current post-download check for missing/inaccurate metadata, avoids that work when size metadata is available.</violation>
</file>

<file name="apps/dify-plugin/tools/git_clone.yaml">

<violation number="1" location="apps/dify-plugin/tools/git_clone.yaml:111">
P2: In Agent apps, private-repository passwords/tokens become model-inferred tool arguments because this secret uses `form: llm`. Using `form: form` would let users preset the credential through the encrypted input instead of supplying it to the model.</violation>
</file>

<file name="apps/dify-plugin/tools/start_service.yaml">

<violation number="1" location="apps/dify-plugin/tools/start_service.yaml:45">
P2: Web servers launched from this guidance may be unreachable through `get_preview_url` when their default host is localhost. Mention that previewable services must bind to `0.0.0.0` so the LLM generates a reachable command.</violation>
</file>

<file name="apps/dify-plugin/tools/list_files.py">

<violation number="1" location="apps/dify-plugin/tools/list_files.py:27">
P2: Listing a directory with many entries can create an oversized tool response or exhaust the plugin's 256 MiB allocation because every result is retained in duplicate JSON and text representations. Consider adding a bounded `limit`/pagination contract and reporting truncation instead of emitting the full listing twice.</violation>
</file>

<file name="apps/dify-plugin/tools/git_clone.py">

<violation number="1" location="apps/dify-plugin/tools/git_clone.py:24">
P2: When both inputs are set, the tool still sends and reports `branch` even though `commit_id` produces a detached HEAD and is documented to override it. Deriving `branch` only after checking `commit_id` keeps the request and result consistent with the tool contract.</violation>

<violation number="2" location="apps/dify-plugin/tools/git_clone.py:44">
P1: Clone URLs containing embedded HTTPS credentials expose the username/token in both JSON and text tool output. Sanitize URL userinfo for both messages while retaining the original URL only for `sandbox.git.clone`.</violation>
</file>

<file name="apps/dify-plugin/tools/search_files.py">

<violation number="1" location="apps/dify-plugin/tools/search_files.py:34">
P2: Broad searches can emit an unbounded file list in one Dify workflow/LLM message, producing oversized payloads for large sandbox trees. Consider adding a result limit or pagination and returning truncation/total metadata.</violation>
</file>

<file name="apps/dify-plugin/tools/start_service.py">

<violation number="1" location="apps/dify-plugin/tools/start_service.py:26">
P2: Two services started in the same sandbox during one second receive the same `session_id`, so the second `create_session` call collides with the first instead of launching its command. Concurrent agent/tool calls make this reachable even without unusually fast manual use. A UUID-based session ID, as used by the ADK implementation, will keep service sessions unique.</violation>

<violation number="2" location="apps/dify-plugin/tools/start_service.py:27">
P2: If asynchronous command launch fails after session creation, the empty session remains in the sandbox and repeated failures accumulate stale session resources. Cleaning up the newly created session on this error path would keep creation transactional.</violation>
</file>

<file name="apps/dify-plugin/tools/run_code.py">

<violation number="1" location="apps/dify-plugin/tools/run_code.py:41">
P2: Large or numerous generated charts can exhaust the long-lived plugin process because every base64 PNG is decoded and emitted without the repository's `MAX_FILE_SIZE` guard. Consider bounding each decoded image and the aggregate chart payload before creating blob messages.</violation>

<violation number="2" location="apps/dify-plugin/tools/run_code.py:56">
P3: Chart blobs and the new JSON fields are absent from both the Run Code tool description and README output contract, so users and the LLM cannot discover the new behavior from plugin metadata. Consider documenting `charts_count`, `charts`, and emitted PNG blobs alongside this response change.</violation>
</file>

<file name="apps/dify-plugin/tools/manage_sandbox.py">

<violation number="1" location="apps/dify-plugin/tools/manage_sandbox.py:21">
P2: Sandbox lookup errors can leak raw Daytona exceptions here because this tool calls `daytona.get` directly instead of `_client.get_sandbox`. Using `get_sandbox` for both lookups would keep error messages and failure behavior consistent with the other tools.</violation>

<violation number="2" location="apps/dify-plugin/tools/manage_sandbox.py:24">
P3: The start path currently waits twice, which adds extra status polling and a second timeout/failure point after a successful `start()`. Since `Sandbox.start()` already blocks until ready, removing the explicit `wait_for_sandbox_start()` keeps behavior simpler and more reliable.</violation>

<violation number="3" location="apps/dify-plugin/tools/manage_sandbox.py:29">
P1: Archiving a running sandbox fails because Daytona requires the sandbox to be stopped before `Sandbox.archive()` is called. Since `archive` is exposed here as a standalone lifecycle action, the common path of archiving a currently running sandbox cannot complete. The archive branch should inspect the current state and stop/wait for the sandbox first (or return a clear stopped-state precondition).</violation>
</file>

<file name="apps/dify-plugin/tools/get_preview_url.yaml">

<violation number="1" location="apps/dify-plugin/tools/get_preview_url.yaml:45">
P3: The README Tool Reference still advertises the old 3000–9999 restriction, so users will not know that ports 1–65535 are now accepted. The documented `get_preview_url` range should be updated with this schema change.</violation>
</file>

<file name="apps/dify-plugin/tools/find_in_files.py">

<violation number="1" location="apps/dify-plugin/tools/find_in_files.py:27">
P2: A broad recursive content search can exhaust the plugin's 256 MiB process or produce an unusably large Dify tool result because every match is copied and returned without a result limit. The Daytona endpoint has no pagination/limit argument, so the wrapper should expose and enforce a bounded `max_results` (and report truncation); if large API responses are expected, a narrower search strategy is also needed before constructing the output.</violation>
</file>

<file name="apps/dify-plugin/tools/list_files.yaml">

<violation number="1" location="apps/dify-plugin/tools/list_files.yaml:45">
P3: The tool schemas exclude valid relative paths by describing `path` as absolute-only. Daytona resolves relative paths from the sandbox working directory for `list_files`, `search_files`, and `find_files`, and these wrappers pass the value through unchanged. Describing this as “a directory path (absolute or relative to the sandbox working directory)” across all three schemas would expose the SDK's actual behavior.</violation>
</file>

<file name="apps/dify-plugin/tools/find_in_files.yaml">

<violation number="1" location="apps/dify-plugin/tools/find_in_files.yaml:15">
P2: Find in Files currently promises regex support even though Daytona's SDK and Toolbox API only document this input as a text/search pattern. An agent may therefore generate regex such as `TODO|FIXME` and silently receive results that do not match the advertised behavior. The descriptions should call this a text pattern unless the supported regex syntax and behavior can be verified.</violation>
</file>

<file name="apps/dify-plugin/tools/list_sandboxes.py">

<violation number="1" location="apps/dify-plugin/tools/list_sandboxes.py:20">
P2: Fractional and non-positive `limit` values are silently truncated or passed to the SDK instead of being rejected; for example, `2.9` becomes `2`, while `0` and negative values form invalid page sizes. This differs from the plugin's existing whole-number validation in `_client.to_int`. Validating that `limit` is a positive whole number will make malformed tool calls fail clearly rather than change meaning or surface an SDK/API error.</violation>

<violation number="2" location="apps/dify-plugin/tools/list_sandboxes.py:23">
P1: `limit` does not currently cap the returned sandboxes: `ListSandboxesQuery.limit` is the pagination page size, and converting the complete `daytona.list(...)` iterator to a list exhausts every page. For accounts with more than 50 sandboxes, the default call therefore returns all of them despite the tool contract and can produce a much larger response. Stop consuming the iterator once `limit` items have been collected.</violation>
</file>

<file name="apps/dify-plugin/tools/read_file.py">

<violation number="1" location="apps/dify-plugin/tools/read_file.py:23">
P1: `read_file` bypasses the 100 MB bound enforced by `download_file`, so a sandbox file of arbitrary size is loaded into memory, decoded into another large allocation, and emitted as a Dify message. This can exhaust the long-lived plugin process. The tool should import `MAX_FILE_SIZE`, check `get_file_info(remote_path).size` before downloading, and retain a post-download `len(file_data)` check as `DownloadFileTool` does.</violation>
</file>

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

Re-trigger cubic



def _cache_key(credentials: dict[str, Any]) -> str:
raw = f"{credentials.get('api_key', '')}:{credentials.get('api_url', '')}"

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: Distinct api_key/api_url pairs can resolve to the same cache entry, causing a tool call to reuse another credential set's authenticated Daytona client. Encode field boundaries unambiguously before hashing, such as with length-prefixed values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/_client.py, line 14:

<comment>Distinct `api_key`/`api_url` pairs can resolve to the same cache entry, causing a tool call to reuse another credential set's authenticated Daytona client. Encode field boundaries unambiguously before hashing, such as with length-prefixed values.</comment>

<file context>
@@ -1,6 +1,18 @@
+
+
+def _cache_key(credentials: dict[str, Any]) -> str:
+    raw = f"{credentials.get('api_key', '')}:{credentials.get('api_url', '')}"
+    return hashlib.sha256(raw.encode()).hexdigest()
 
</file context>
Suggested change
raw = f"{credentials.get('api_key', '')}:{credentials.get('api_url', '')}"
api_key = str(credentials.get("api_key", ""))
api_url = str(credentials.get("api_url", ""))
raw = f"{len(api_key)}:{api_key}{len(api_url)}:{api_url}"
Fix with cubic

"exit_code": response.exit_code,
"stdout": response.stdout or "",
"stderr": response.stderr or "",
"output": response.result or "",

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: Run Command no longer honors its public response contract: callers expecting stdout and stderr now receive an undocumented combined output field instead. Preserve separate stream execution/results and return the documented keys to avoid breaking existing Dify workflows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/run_command.py, line 38:

<comment>Run Command no longer honors its public response contract: callers expecting `stdout` and `stderr` now receive an undocumented combined `output` field instead. Preserve separate stream execution/results and return the documented keys to avoid breaking existing Dify workflows.</comment>

<file context>
@@ -17,37 +15,46 @@ def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessag
                 "exit_code": response.exit_code,
-                "stdout": response.stdout or "",
-                "stderr": response.stderr or "",
+                "output": response.result or "",
                 "sandbox_id": sandbox.id,
             })
</file context>
Fix with cubic

)

content = sandbox.fs.download_file(remote_path)
if len(content) > MAX_FILE_SIZE:

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: An oversized or concurrently growing sandbox file can still exhaust the plugin worker's memory because the size limit is checked only after download_file() has materialized all bytes. Consider downloading through a bounded streaming/temp-file path and aborting once MAX_FILE_SIZE is exceeded, while retaining the metadata precheck as an optimization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/download_file.py, line 33:

<comment>An oversized or concurrently growing sandbox file can still exhaust the plugin worker's memory because the size limit is checked only after `download_file()` has materialized all bytes. Consider downloading through a bounded streaming/temp-file path and aborting once `MAX_FILE_SIZE` is exceeded, while retaining the metadata precheck as an optimization.</comment>

<file context>
@@ -21,7 +21,20 @@ def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessag
+            )
+
         content = sandbox.fs.download_file(remote_path)
+        if len(content) > MAX_FILE_SIZE:
+            raise ValueError(
+                f"Downloaded file size ({len(content)} bytes) exceeds maximum allowed size "
</file context>
Fix with cubic

yield self.create_json_message({
"success": True,
"sandbox_id": sandbox_id,
"url": url,

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: Clone URLs containing embedded HTTPS credentials expose the username/token in both JSON and text tool output. Sanitize URL userinfo for both messages while retaining the original URL only for sandbox.git.clone.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/git_clone.py, line 44:

<comment>Clone URLs containing embedded HTTPS credentials expose the username/token in both JSON and text tool output. Sanitize URL userinfo for both messages while retaining the original URL only for `sandbox.git.clone`.</comment>

<file context>
@@ -0,0 +1,50 @@
+        yield self.create_json_message({
+            "success": True,
+            "sandbox_id": sandbox_id,
+            "url": url,
+            "path": path,
+            "branch": branch,
</file context>
Fix with cubic

elif action == "stop":
sandbox.stop()
elif action == "archive":
sandbox.archive()

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: Archiving a running sandbox fails because Daytona requires the sandbox to be stopped before Sandbox.archive() is called. Since archive is exposed here as a standalone lifecycle action, the common path of archiving a currently running sandbox cannot complete. The archive branch should inspect the current state and stop/wait for the sandbox first (or return a clear stopped-state precondition).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/manage_sandbox.py, line 29:

<comment>Archiving a running sandbox fails because Daytona requires the sandbox to be stopped before `Sandbox.archive()` is called. Since `archive` is exposed here as a standalone lifecycle action, the common path of archiving a currently running sandbox cannot complete. The archive branch should inspect the current state and stop/wait for the sandbox first (or return a clear stopped-state precondition).</comment>

<file context>
@@ -0,0 +1,46 @@
+        elif action == "stop":
+            sandbox.stop()
+        elif action == "archive":
+            sandbox.archive()
+        else:
+            raise ValueError(
</file context>
Fix with cubic

"exit_code": response.exit_code,
"output": response.result,
"sandbox_id": sandbox.id,
"charts_count": len(charts_meta),

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: Chart blobs and the new JSON fields are absent from both the Run Code tool description and README output contract, so users and the LLM cannot discover the new behavior from plugin metadata. Consider documenting charts_count, charts, and emitted PNG blobs alongside this response change.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/run_code.py, line 56:

<comment>Chart blobs and the new JSON fields are absent from both the Run Code tool description and README output contract, so users and the LLM cannot discover the new behavior from plugin metadata. Consider documenting `charts_count`, `charts`, and emitted PNG blobs alongside this response change.</comment>

<file context>
@@ -26,10 +27,34 @@ def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessag
                 "exit_code": response.exit_code,
                 "output": response.result,
                 "sandbox_id": sandbox.id,
+                "charts_count": len(charts_meta),
+                "charts": charts_meta,
             })
</file context>
Fix with cubic

sandbox = daytona.get(sandbox_id)

if action == "start":
sandbox.start()

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: The start path currently waits twice, which adds extra status polling and a second timeout/failure point after a successful start(). Since Sandbox.start() already blocks until ready, removing the explicit wait_for_sandbox_start() keeps behavior simpler and more reliable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/manage_sandbox.py, line 24:

<comment>The start path currently waits twice, which adds extra status polling and a second timeout/failure point after a successful `start()`. Since `Sandbox.start()` already blocks until ready, removing the explicit `wait_for_sandbox_start()` keeps behavior simpler and more reliable.</comment>

<file context>
@@ -0,0 +1,46 @@
+        sandbox = daytona.get(sandbox_id)
+
+        if action == "start":
+            sandbox.start()
+            sandbox.wait_for_sandbox_start()
+        elif action == "stop":
</file context>
Fix with cubic

ja_JP: The port number inside the sandbox where the service is listening (e.g. 3000, 5000, 8080).
pt_BR: The port number inside the sandbox where the service is listening (e.g. 3000, 5000, 8080).
llm_description: 'The port number inside the sandbox where the service is listening (e.g. 3000 for Next.js, 5000 for Flask, 8080 for generic HTTP, 8501 for Streamlit). Range 3000-9999.'
llm_description: 'The port number inside the sandbox where the service is listening (e.g. 3000 for Next.js, 5000 for Flask, 8080 for generic HTTP, 8501 for Streamlit). Range 1-65535.'

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: The README Tool Reference still advertises the old 3000–9999 restriction, so users will not know that ports 1–65535 are now accepted. The documented get_preview_url range should be updated with this schema change.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/get_preview_url.yaml, line 45:

<comment>The README Tool Reference still advertises the old 3000–9999 restriction, so users will not know that ports 1–65535 are now accepted. The documented `get_preview_url` range should be updated with this schema change.</comment>

<file context>
@@ -42,7 +42,7 @@ parameters:
       ja_JP: The port number inside the sandbox where the service is listening (e.g. 3000, 5000, 8080).
       pt_BR: The port number inside the sandbox where the service is listening (e.g. 3000, 5000, 8080).
-    llm_description: 'The port number inside the sandbox where the service is listening (e.g. 3000 for Next.js, 5000 for Flask, 8080 for generic HTTP, 8501 for Streamlit). Range 3000-9999.'
+    llm_description: 'The port number inside the sandbox where the service is listening (e.g. 3000 for Next.js, 5000 for Flask, 8080 for generic HTTP, 8501 for Streamlit). Range 1-65535.'
     form: llm
 extra:
</file context>
Fix with cubic

@@ -0,0 +1,49 @@
identity:

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: The tool schemas exclude valid relative paths by describing path as absolute-only. Daytona resolves relative paths from the sandbox working directory for list_files, search_files, and find_files, and these wrappers pass the value through unchanged. Describing this as “a directory path (absolute or relative to the sandbox working directory)” across all three schemas would expose the SDK's actual behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/list_files.yaml, line 45:

<comment>The tool schemas exclude valid relative paths by describing `path` as absolute-only. Daytona resolves relative paths from the sandbox working directory for `list_files`, `search_files`, and `find_files`, and these wrappers pass the value through unchanged. Describing this as “a directory path (absolute or relative to the sandbox working directory)” across all three schemas would expose the SDK's actual behavior.</comment>

<file context>
@@ -0,0 +1,49 @@
+      zh_Hans: 'Absolute directory path inside the sandbox (e.g. /home/daytona or /tmp).'
+      ja_JP: 'Absolute directory path inside the sandbox (e.g. /home/daytona or /tmp).'
+      pt_BR: 'Absolute directory path inside the sandbox (e.g. /home/daytona or /tmp).'
+    llm_description: 'Absolute directory path inside the sandbox to list (e.g. "/home/daytona", "/tmp", "/").'
+    form: llm
+extra:
</file context>
Fix with cubic

ja_JP: Run a shell command in a Daytona sandbox and return the output.
pt_BR: Run a shell command in a Daytona sandbox and return the output.
llm: Execute a shell command in a Daytona sandbox. Useful for installing packages, running scripts, file operations, and any Linux command. If no sandbox_id is provided, a new ephemeral sandbox is created automatically. Returns stdout, stderr, and exit code.
llm: Execute a shell command in a Daytona sandbox. Useful for installing packages, running scripts, file operations, and any Linux command. Supports optional working directory (cwd) and environment variables. If no sandbox_id is provided, a new ephemeral sandbox is created automatically. Returns combined output (stdout and stderr) and exit code.

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 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: The public README still promises separate stdout and stderr fields, while this changed tool contract now returns only combined output. Updating the Run Command feature and Tool Reference alongside this schema change would prevent workflow authors from building against fields the tool no longer emits.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dify-plugin/tools/run_command.yaml, line 15:

<comment>The public README still promises separate `stdout` and `stderr` fields, while this changed tool contract now returns only combined `output`. Updating the Run Command feature and Tool Reference alongside this schema change would prevent workflow authors from building against fields the tool no longer emits.</comment>

<file context>
@@ -12,7 +12,7 @@ description:
     ja_JP: Run a shell command in a Daytona sandbox and return the output.
     pt_BR: Run a shell command in a Daytona sandbox and return the output.
-  llm: Execute a shell command in a Daytona sandbox. Useful for installing packages, running scripts, file operations, and any Linux command. If no sandbox_id is provided, a new ephemeral sandbox is created automatically. Returns stdout, stderr, and exit code.
+  llm: Execute a shell command in a Daytona sandbox. Useful for installing packages, running scripts, file operations, and any Linux command. Supports optional working directory (cwd) and environment variables. If no sandbox_id is provided, a new ephemeral sandbox is created automatically. Returns combined output (stdout and stderr) and exit code.
 parameters:
   - name: command
</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