Problem
A single harnessd instance is bound to a single workspace at startup (`HARNESS_WORKSPACE` env var, default cwd). The tool catalog is built once with `BuildOptions{WorkspaceRoot: workspace}` (`cmd/harnessd/main.go:447`) and every tool — Read, Write, Edit, Bash, Grep, Ls, Glob — resolves paths relative to that single root. Two runs against two different projects step on each other's files.
Today's options:
- One harnessd per project — works, but burns a process per repo (~26 MB RSS each).
- `workspace_type: "worktree"` — provisions a git worktree per run, but only useful for branches off the SAME base repo (`baseOpts.RepoPath` is set once at startup).
- `workspace_type: "container"` — passes `validateWorkspaceType` but `provisionRunWorkspace` errors with "unknown workspace type" in standalone harnessd. Container mode is only wired through `symphd`.
- `symphd` — the orchestrator does support multi-project dispatch via the workspace pool. But that's a heavier setup than "I just want to point my one harnessd at two repos."
Proposal
Add a `workspace_path` field to `RunRequest` (`internal/harness/types.go:268`) that overrides the runner's startup workspace for that single run. No provisioning, no isolation — just rebind the tool catalog's `WorkspaceRoot` per run.
```go
type RunRequest struct {
// ... existing fields ...
// WorkspaceRoot, when set, overrides the harnessd's startup workspace
// for filesystem and shell tools in this run. Path must be absolute and
// exist; relative paths are rejected at StartRun time. Empty falls back
// to the server's startup HARNESS_WORKSPACE.
//
// This is a non-isolating override: the run shares the host filesystem
// with the parent process. For isolation use WorkspaceType ("worktree",
// "container", "vm") instead.
WorkspaceRoot string \`json:"workspace_root,omitempty"\`
}
```
Wiring sketch
The tool catalog is currently built once at startup (`cmd/harnessd/main.go:447` calls `harness.NewDefaultRegistryWithOptions(workspace, ...)`). To honor per-request overrides, the runner needs to either:
- Option A (simpler): at StartRun time, when `req.WorkspaceRoot` is non-empty and ≠ startup workspace, build a fresh tool registry scoped to that path and stash it on the runState. `BuildToolList` already takes `BuildOptions` so this is mostly threading.
- Option B (more invasive): parameterize `WorkspaceRoot` on every tool call rather than baking it at registry construction time. Bigger refactor; better long-term.
Option A is the smallest change and fits the existing pattern.
Validation
- `req.WorkspaceRoot` must be absolute (`filepath.IsAbs`).
- Must exist and be a directory (`os.Stat`).
- Should be cleaned (`filepath.Clean`) and resolved (`filepath.EvalSymlinks`) before binding.
- Optional safety: a startup config knob `workspace_root_allowlist` to restrict which paths are accepted (defense against using harnessd as an arbitrary-fs-access proxy across tenant boundaries).
Acceptance
- POST /v1/runs with `{"prompt": "...", "workspace_root": "/some/other/repo"}` runs all tools rooted at `/some/other/repo`.
- Two runs with different `workspace_root` values, started in flight, do not interfere.
- Tool errors (e.g. `Read` of a path outside `workspace_root`) are scoped to that run's root, not the startup root.
- Smoke test: build the URL shortener (`/tmp/harness-demo-shortener`) and the calc test (`/tmp/harness-sandbox-20260429/repo`) concurrently against one harnessd.
References
- `internal/harness/types.go:268` — RunRequest struct
- `cmd/harnessd/main.go:447` — registry construction
- `internal/harness/runner.go:683` — `provisionRunWorkspace` flow (separate from this; this issue is about non-provisioning rebind)
- Investigation: `docs/investigations/2026-04-29-harness-real-world-build.md` — proves single-project flow works end-to-end via direct API.
Problem
A single
harnessdinstance is bound to a single workspace at startup (`HARNESS_WORKSPACE` env var, default cwd). The tool catalog is built once with `BuildOptions{WorkspaceRoot: workspace}` (`cmd/harnessd/main.go:447`) and every tool — Read, Write, Edit, Bash, Grep, Ls, Glob — resolves paths relative to that single root. Two runs against two different projects step on each other's files.Today's options:
Proposal
Add a `workspace_path` field to `RunRequest` (`internal/harness/types.go:268`) that overrides the runner's startup workspace for that single run. No provisioning, no isolation — just rebind the tool catalog's `WorkspaceRoot` per run.
```go
type RunRequest struct {
// ... existing fields ...
}
```
Wiring sketch
The tool catalog is currently built once at startup (`cmd/harnessd/main.go:447` calls `harness.NewDefaultRegistryWithOptions(workspace, ...)`). To honor per-request overrides, the runner needs to either:
Option A is the smallest change and fits the existing pattern.
Validation
Acceptance
References