Skip to content

bug(soundwave/launcher): local-path targets not staged into sandbox — analyst falls back to black-box with no warning #202

@Lempkey

Description

@Lempkey

Summary

When a local filesystem path is given as the target during the Soundwave interview, the path is recorded in the RoE but never copied into the sandbox container. The analyst agent receives the path via the RoE but cannot reach it — the host filesystem is not visible inside the container — so it silently falls back to black-box-only analysis with no error or warning to the operator.

`docs/engagement-workflow.md` explicitly lists Local path (`/path/to/target/app`) as a supported target type, so this is a documented capability gap, not a missing feature.


Steps to Reproduce

  1. Start a new engagement via `make dogfood` (or `decepticon`).
  2. During the Soundwave interview, enter a local filesystem path as the target (e.g. `/Users//Projects/myapp/services/api`).
  3. Approve the generated RoE / ConOps / OPPLAN.
  4. Let the engagement run. The analyst agent starts OBJ-002 (source code static analysis).

Expected: Analyst finds source at a well-known path inside the sandbox (e.g. `/workspace/src`) and performs white-box analysis.

Actual: Analyst logs something like:

```
The host path is not mounted in this container.
Confirming this is a genuine engagement issue: the RoE specifies the source
path as /Users/... but the sandbox can't reach it.
```

OBJ-002 is blocked. OBJ-003 (exploit) proceeds with black-box only — severely degrading the engagement quality.


Root Cause

Three layers of the stack each know about the local path but none of them act on it:

1. Soundwave — planning-only, no file ops (by design)

`soundwave.py` and its skills (`ask_user.py`, `complete_planning.py`, etc.) write planning documents (RoE/ConOps/OPPLAN) but have no file-copy or workspace-initialization logic. Rule 1 of the Soundwave skill: "No Execution." Correct by design — but the hand-off to a staging step never happens.

2. Launcher — no source staging step

`clients/launcher/internal/engagement/picker.go` creates the engagement workspace and sets `DECEPTICON_ENGAGEMENT_WORKSPACE`, but after Soundwave finishes and before the autonomous loop starts, there is no step that reads the RoE for `local_path` / `source_code` target types and copies the source tree into the workspace.

3. `docker-compose.yml` — static workspace mount only

```yaml

sandbox service

volumes:

  • ${DECEPTICON_ENGAGEMENT_WORKSPACE:-${DECEPTICON_HOME:-~/.decepticon}/workspace}:/workspace
    ```

The workspace is bound at compose-up time. There is no dynamic bind-mount injection per engagement, and no path for a local source tree to reach `/workspace/src` automatically.

Existing primitive that could be reused

`decepticon/backends/docker_sandbox.py` already implements `upload_files()` using `docker cp`:

```python
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
# Writes content via docker cp into the sandbox
subprocess.run(["docker", "cp", tmp_path, f"{self._container_name}:{path}"])
```

This is the right tool — it just needs to be called during engagement initialization for local-path targets.


Impact

  • White-box engagements silently degrade to black-box. The operator gets no error; they only discover the problem when the analyst reports the blocker after spending time on recon.
  • Auth schemes are opaque without source. In a real case: the analyst ran 107+ black-box probes against a FastAPI service and couldn't find the auth header because it used a non-standard name (`X-Internal-Key`) that was only visible in the client-side source code. White-box access would have found this immediately.
  • Affects the VPS install path too. Even if the operator's source tree lives on a remote host, there's no way to stage it short of manual `docker cp`.

Proposed Fix

A pre-execution staging hook in the launcher, after Soundwave finishes and before the autonomous loop starts:

Pseudocode (launcher, Go):
```go
// After engagement documents are approved:
roe := loadROE(engagementWorkspace)
for _, scope := range roe.Scope {
if scope.Type == "local_path" || scope.Type == "source_code" {
srcDir := scope.Target // e.g. /Users/.../myapp
dstDir := filepath.Join(engagementWorkspace, "src")
if err := stagePath(srcDir, dstDir); err != nil {
log.Warn("Could not stage source path — continuing without white-box access", "path", srcDir, "err", err)
}
}
}
```

Because `/workspace` is already bind-mounted to the engagement workspace, anything copied to `/src/` is immediately visible inside the sandbox at `/workspace/src/`.

Alternative (decepticon skill): A `setup_source` skill that runs as the first step of OBJ-002, reads the RoE's local path, and calls `upload_files()` via the existing `DockerSandboxBackend`. Slower (copies happen inside the agent loop) but requires no changes to the launcher.

Either approach should also:

  • Respect `:ro` semantics — source is read-only for the analyst.
  • Warn clearly if the path doesn't exist or is inaccessible (don't silently skip).
  • Document the staged path in the engagement logs so the operator can verify it.

Environment

  • Decepticon: latest `main` (local clone, `make dogfood`)
  • Docker: 29.x, macOS (Apple Silicon)
  • Python: 3.13
  • Engagement type: white-box web app (local source + live service)

Workaround (until fixed)

Manually copy the source into the running sandbox after Soundwave finishes and before re-dispatching OBJ-002:

```bash
docker cp /path/to/your/source/. decepticon-sandbox:/workspace/src/
```

Then tell the agent: "Source is now available at `/workspace/src`. Re-dispatch OBJ-002."

For persistence across restarts, add to `docker-compose.override.yml`:

```yaml
services:
sandbox:
volumes:
- /path/to/your/source:/workspace/src:ro
```

And symlink the override into `.dogfood/` if using `make dogfood`:

```bash
ln -sfn /path/to/Decepticon/docker-compose.override.yml
/path/to/Decepticon/.dogfood/docker-compose.override.yml
```

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions