Skip to content

feat(execd): add replacedCount feedback to Replace File Content API#991

Open
Pangjiping wants to merge 3 commits into
opensandbox-group:mainfrom
Pangjiping:feat/replace-file-api-feedback
Open

feat(execd): add replacedCount feedback to Replace File Content API#991
Pangjiping wants to merge 3 commits into
opensandbox-group:mainfrom
Pangjiping:feat/replace-file-api-feedback

Conversation

@Pangjiping
Copy link
Copy Markdown
Collaborator

Summary

  • Add replacedCount field per file in the POST /files/replace response, so callers can detect no-match and multi-match scenarios
  • Update all language SDKs (Python, JavaScript, Go, Kotlin, C#) and MCP server to expose the new response
  • Add e2e test coverage for no-match (replacedCount=0), multi-match (replacedCount>1), and batch replace across all test suites

Closes #982

Changes

Layer What changed
OpenAPI spec execd-api.yaml — 200 response schema + ReplaceFileContentResult
execd handler strings.Count() before replace, return per-file results (unix + windows)
Python SDK New ContentReplaceResult model, adapter/service return list[ContentReplaceResult]
JS SDK New ContentReplaceResult interface, adapter/service return ContentReplaceResult[]
Go SDK New ReplaceResult/ReplaceResponse types, ReplaceInFiles() returns (ReplaceResponse, error)
Kotlin SDK New ContentReplaceResult data class, adapter uses direct OkHttp call to parse response
C# SDK New ContentReplaceResult class, adapter returns IReadOnlyList<ContentReplaceResult>
MCP server Returns list[ContentReplaceResult] instead of StatusResponse
E2E tests All 6 test suites (Python async/sync, JS, Go, Java, C#) — assert return values + 3 new scenarios

Backward compatibility

  • Old SDK + new execd: safe — old SDK ignores response body
  • New SDK + old execd: Python handles empty body gracefully; JS returns []; Go decodes empty → nil map
  • Response going from empty to JSON body is non-breaking
  • SDK return type changes from void/None to result list — callers not using return value are unaffected

Test plan

  • go build ./... in components/execd — passed locally
  • pnpm run build in sdks/sandbox/javascript — passed locally
  • Python import check — passed locally
  • E2E tests require running sandbox — verify in CI
  • Kotlin ./gradlew build to regenerate API from spec

🤖 Generated with Claude Code

The Replace File Content API (`POST /files/replace`) previously returned
HTTP 200 with an empty body, making it impossible for callers to know
whether replacements actually occurred. This change adds a `replacedCount`
field per file in the response so callers can detect no-match scenarios.

Closes opensandbox-group#982

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb55dcab0d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread sdks/sandbox/go/execd.go
return e.client.doRequest(ctx, http.MethodPost, "/files/replace", req, nil)
func (e *ExecdClient) ReplaceInFiles(ctx context.Context, req ReplaceRequest) (ReplaceResponse, error) {
var resp ReplaceResponse
err := e.client.doRequest(ctx, http.MethodPost, "/files/replace", req, &resp)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve empty-body compatibility for replace responses

When this SDK talks to an execd version from before this API addition, /files/replace still returns 200 OK with an empty body because the old controller called RespondSuccess(nil). Passing a non-nil result here makes doRequest run json.NewDecoder(resp.Body).Decode(&resp), so those otherwise-successful replacements now fail with decode response: EOF; Python/Kotlin explicitly handle this compatibility case, but the Go path does not.

Useful? React with 👍 / 👎.

Comment on lines +270 to +271
var response = await _client.PostAsync<Dictionary<string, ReplaceFileContentResult>>(
"/files/replace", body, cancellationToken).ConfigureAwait(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve empty-body compatibility for replace responses

When the C# SDK is used against an execd build that predates replacedCount, /files/replace returns 200 OK with no response body (RespondSuccess(nil) in the old handler). Switching to the generic PostAsync<T> path makes HandleResponseAsync<T> throw Unexpected empty response body, so successful replacements on older sandboxes now surface as SDK failures instead of returning an empty result list.

Useful? React with 👍 / 👎.


// ReplaceInFiles performs text replacement in files.
func (s *Sandbox) ReplaceInFiles(ctx context.Context, req ReplaceRequest) error {
func (s *Sandbox) ReplaceInFiles(ctx context.Context, req ReplaceRequest) (ReplaceResponse, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid breaking Go ReplaceInFiles callers

Changing this public SDK method from error to (ReplaceResponse, error) breaks every existing Go caller that currently does err := sandbox.ReplaceInFiles(...) or passes the method as a func(...) error. Since the replacement counts are additive feedback, this should keep the existing method signature and expose the counted response through a new API or other backward-compatible path.

Useful? React with 👍 / 👎.

/// <exception cref="InvalidArgumentException">Thrown when request values are invalid.</exception>
/// <exception cref="SandboxException">Thrown when the execd service request fails.</exception>
Task ReplaceContentsAsync(
Task<IReadOnlyList<ContentReplaceResult>> ReplaceContentsAsync(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update the C# test double for the new interface

This interface change requires every in-repo ISandboxFiles implementation to return Task<IReadOnlyList<ContentReplaceResult>>, but StubFiles in sdks/sandbox/csharp/tests/OpenSandbox.Tests/SandboxEgressLifecycleTests.cs:370 still implements Task ReplaceContentsAsync(...). As a result, the C# test project no longer compiles once this signature is changed.

Useful? React with 👍 / 👎.

if (body.isNullOrBlank()) {
emptyList()
} else {
val parsed = kotlinx.serialization.json.Json.decodeFromString<Map<String, kotlinx.serialization.json.JsonObject>>(body)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Import the Kotlin decodeFromString extension

This call relies on the reified kotlinx.serialization.decodeFromString extension, but that extension is not imported in this file; the member overload on Json requires an explicit deserializer. In the Kotlin SDK build this leaves the new replace response parsing line without a matching decodeFromString overload, so :sandbox:compileKotlin fails before the SDK can be published.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

@Pangjiping Pangjiping left a comment

Choose a reason for hiding this comment

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

Self-review: found 6 issues (2× P1, 2× P2, 2× P3). P1 backward compat bugs in Go and C# SDKs need fixing before merge.

Comment thread components/execd/pkg/web/controller/filesystem.go
Comment thread sdks/sandbox/go/execd.go
Comment thread sdks/sandbox/csharp/src/OpenSandbox/Adapters/FilesystemAdapter.cs Outdated
Comment thread sdks/sandbox/python/src/opensandbox/api/execd/api/filesystem/replace_content.py Outdated
- P1: Go SDK — handle io.EOF when old execd returns empty body
- P1: C# SDK — catch JsonException on empty body deserialization
- P2: execd handler — validate item.Old != "" to prevent misleading
  replacedCount from strings.Count("", s) returning len+1
- P2: Python SDK — move backward compat from codegen file to adapter
  layer (catch JSONDecodeError instead of patching generated code)
- P3: Kotlin — use jsonPrimitive.intOrNull instead of toString().toIntOrNull()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2be6f1989

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +427 to +428
except JSONDecodeError:
return []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't treat error responses as empty replace results

This catch wraps asyncio_detailed, whose generated parser calls response.json() for 400 and 500 responses as well as 200 responses. If execd/Gin or an intermediary returns an empty or non-JSON 400/500 body, this returns [] before handle_api_error runs, so a failed replacement is reported as a successful no-result replacement; the sync adapter repeats the same pattern. Limit the empty-body compatibility path to successful responses only.

Useful? React with 👍 / 👎.

- C# StubFiles: update ReplaceContentsAsync return type to match interface
- Kotlin: apply spotless formatting (line breaks, indentation)
- Java E2E: fix method name writeFiles → write to match Kotlin interface

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ca9909396

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +364 to +366
if item.Old == "" {
c.RespondError(http.StatusBadRequest, model.ErrorCodeInvalidRequest, "old content must not be empty")
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve empty old-content compatibility

For clients that send old: "", this now returns 400 even though the public contract still defines old as a plain required string with no minLength (checked specs/execd-api.yaml), and the SDK models still allow constructing that request. The previous handler passed the value through to strings.ReplaceAll, so this is an undocumented breaking protocol change; either preserve the old behavior or update the spec/SDK validation with a migration story.

Useful? React with 👍 / 👎.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace File API: Add replacement result feedback

1 participant