Host-side API server experiment#28
Conversation
9197ac7 to
038e446
Compare
ReviewFindingsCritical
High
Medium
Low
Previous runReviewOutcome: comment · 4 high · 5 medium · 3 low · 5 info This PR adds a well-structured experiment testing host-side API servers callable from inside OpenShell sandboxes. The experiment design is solid, the shell orchestration is clean ( HighH1 · Argument injection via unvalidated request fields —
|
- Scrub credentials from error messages in provisioner (H3) - Add timing-safe comparison comments in both servers (M1) - Bind servers to 127.0.0.1 instead of 0.0.0.0 (M2) - Remove bearer token from stdout in run.sh (M3) - Add request body size limits in both servers (M4) - Add comment about unbounded operations dict (L1) - Fix stale host.docker.internal references in design docs (L3) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Marta Anon <manon@redhat.com>
|
Addressed in 4fbee55:
|
Add design spec exploring how sandboxed agents can call host-side API servers through the L7 network proxy, and implementation plan for integrating the experiment with fullsend run. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Marta Anon <manon@redhat.com>
Add builder (Go) and provisioner (Python) API servers with uniform process contract: --port, --token, /healthz, /tools.json, SIGTERM handling. Both use openshell sandbox download/upload for host-sandbox file transfer with per-request sandbox identification. Add fullsend run integration: 6 harness configs (3 discovery methods x 2 network policies), skills for baked-instructions/openapi/tooluse discovery, OpenShell provider for bearer token auth via credential placeholders, L7 policies with host.openshell.internal hostname matching, and run.sh wrapper managing server lifecycle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Marta Anon <manon@redhat.com>
Add JSONL transcripts, replay HTML, and agent output for all 6 harness runs. Add findings.md covering architecture validated (canonical hostname, server contract, provider auth, file transfer, network policies, orchestrator lifecycle), bugs found, token usage analysis, and recommendations for the host-side API server pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Marta Anon <manon@redhat.com>
- Scrub credentials from error messages in provisioner (H3) - Add timing-safe comparison comments in both servers (M1) - Bind servers to 127.0.0.1 instead of 0.0.0.0 (M2) - Remove bearer token from stdout in run.sh (M3) - Add request body size limits in both servers (M4) - Add comment about unbounded operations dict (L1) - Fix stale host.docker.internal references in design docs (L3) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Marta Anon <manon@redhat.com>
4fbee55 to
0df51be
Compare
|
|
||
| localContext := tmpDir + "/context" | ||
| log.Printf("Downloading build context from sandbox %s:%s", sb, req.ContextDir) | ||
| dlCmd := exec.Command("openshell", "sandbox", "download", sb, req.ContextDir, localContext) |
There was a problem hiding this comment.
[critical] command-injection
User-controlled request fields (Tag, Dockerfile, ContextDir, Dest, Sandbox) are passed unsanitized as arguments to exec.Command calls for podman and openshell CLIs. Path traversal via req.Dockerfile (line 164) can escape the temp context directory; req.Tag passed to podman push (line 234) as a positional argument could be interpreted as a CLI flag.
Suggested fix: Validate all request fields: Tag against a strict image-tag regex rejecting leading dashes; Dockerfile via filepath.Base() rejecting path separators and ..; ContextDir and Dest via filepath.Clean() rejecting .. components; Sandbox against an alphanumeric pattern.
|
|
||
|
|
||
| def clone_repo(repo: str, ref: str, dest: str) -> None: | ||
| """Clone a GitHub repo into dest, checking out ref.""" |
There was a problem hiding this comment.
[high] command-injection
The repo parameter is validated only with 'not repo or / not in repo', allowing path traversal and extra path segments. The value is interpolated into the clone URL with an embedded github_token, so a crafted repo could leak the token to an unexpected repository.
Suggested fix: Validate repo against ^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$.
| clone_url = f"https://github.com/{repo}.git" | ||
|
|
||
| subprocess.run( | ||
| ["git", "clone", "--depth", "1", "--branch", ref, clone_url, dest], |
There was a problem hiding this comment.
[high] command-injection
The ref parameter is passed directly to git clone --branch with no validation. While --branch consumes the next argument, the lack of validation is a defense-in-depth failure.
Suggested fix: Validate ref against ^[a-zA-Z0-9_./-]+$ and reject values starting with -. Add -- before the clone URL.
|
|
||
|
|
||
| def upload_to_sandbox_named(sb: str, local_path: str, dest: str) -> None: | ||
| """Upload files into the sandbox via openshell sandbox upload.""" |
There was a problem hiding this comment.
[high] command-injection
The sandbox and dest parameters are passed directly to subprocess.run for openshell sandbox upload with no validation. sandbox starting with -- could be interpreted as a flag; dest could contain path traversal sequences.
Suggested fix: Validate sandbox against alphanumeric-plus-hyphens. Validate dest as a relative path with no .. components. Add -- before positional arguments.
| } | ||
|
|
||
| // NOTE: production code should use crypto/subtle.ConstantTimeCompare | ||
| if parts[1] != bearerToken { |
There was a problem hiding this comment.
[medium] auth-weakness
Bearer token comparison uses != (non-constant-time). Timing side-channel could leak token information.
Suggested fix: Use crypto/subtle.ConstantTimeCompare as noted in the existing code comment.
| with operations_lock: | ||
| operations[op_id] = result | ||
| log(f"Failed to provision {repo}: {exc}") | ||
| return result |
There was a problem hiding this comment.
[medium] credential-exposure
CalledProcessError log message includes the full command with embedded GitHub token in the clone URL. _scrub_credentials() is only applied to the result dict, not the log output.
Suggested fix: Apply _scrub_credentials(str(exc)) in the log call.
| r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) | ||
| var req BuildRequest | ||
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | ||
| http.Error(w, fmt.Sprintf(`{"error":"invalid JSON: %s"}`, err), http.StatusBadRequest) |
There was a problem hiding this comment.
[medium] json-injection
Error responses use fmt.Sprintf to interpolate into JSON string templates. Error messages containing quotes or backslashes produce malformed JSON.
Suggested fix: Use json.Marshal or json.NewEncoder for all JSON responses including errors.
|
|
||
| // Download build context from sandbox | ||
| if sb == "" { | ||
| resp.Status = "failed" |
There was a problem hiding this comment.
[medium] logic-error
buildHandler returns HTTP 200 on all failure paths. Clients must inspect the JSON body to detect failures, violating REST conventions.
Suggested fix: Return appropriate HTTP error status codes (500 for build failures, 400 for validation errors).
| dest: /tmp/workspace/.env.d/gcp-vertex.env | ||
| expand: true | ||
| - src: ${GOOGLE_APPLICATION_CREDENTIALS} | ||
| dest: /tmp/workspace/.gcp-credentials.json |
There was a problem hiding this comment.
[medium] credential-exposure
All six harness files copy the GCP service account key file into the sandbox via host_files, undermining the credential isolation goal stated in the experiment design.
Suggested fix: Document this as a known limitation or use an OpenShell provider for GCP credentials.
Summary
Closes #25
--port,--token,/healthz,/tools.json,SIGTERM),openshell sandbox download/uploadfor file transfer, andfullsend runintegration with 6 harness configs (3 discovery methods × 2 network policies)host.openshell.internalhostname, provider-based bearer token auth, L7 policy structure, orchestrator lifecycle, bugs found, token usage analysis, and recommendation for/tools.jsonas the standard API discovery formatTest plan
baked-instructions-full/restricted,openapi-discovery-full/restricted,tooluse-discovery-full/restricted)🤖 Generated with Claude Code