Skip to content

Commit 94d5dc7

Browse files
authored
feat: make workspace path configurable via CLI flag (#98)
* feat: make workspace path configurable via CLI flag and env var Add --workspace flag and DOCSCLAW_WORKSPACE env var to the serve command, allowing operators to override the workspace directory without editing the ConfigMap. Priority: agent-config.yaml > flag/env > default (/workspace). Unifies workspace resolution into a single resolveWorkspace function, replacing two ad-hoc inline resolution blocks. Closes #96 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> * fix: clarify workspace env var comment and trim whitespace - Clarify DOCSCLAW_WORKSPACE comment in deployment manifest to note it is a fallback, not an override, when agent-config.yaml sets tools.workspace - Trim whitespace in resolveWorkspace to reject blank strings Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> * fix: address CodeRabbit nitpicks on workspace PR - Reword workspace table entry to a complete sentence - Add whitespace-trimming test for resolveWorkspace Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> --------- Signed-off-by: Pavel Anni <panni@redhat.com>
1 parent 853a4bb commit 94d5dc7

4 files changed

Lines changed: 60 additions & 12 deletions

File tree

deploy/standalone-agent.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ spec:
126126
env:
127127
- name: DOCSCLAW_SERVICE_PORT
128128
value: "8000"
129+
# Fallback workspace directory — used only when
130+
# agent-config.yaml does not set tools.workspace:
131+
# - name: DOCSCLAW_WORKSPACE
132+
# value: "/data/agent"
129133
envFrom:
130134
- secretRef:
131135
name: llm-secret

docs/agent-config-guide.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ agent-config.yaml
9999
| Field | Type | Default | Description |
100100
| ----- | ---- | ------- | ----------- |
101101
| `allowed` | `[]string` | `[]` (none) | Tools the LLM may call. See [available tools](#available-tools). |
102-
| `workspace` | `string` | `/workspace` | Root directory for `read_file` and `write_file`. The agent cannot access files outside this path. |
102+
| `workspace` | `string` | `/workspace` | Root directory for `read_file` and `write_file`. The agent cannot access files outside this path. The workspace path can also be set via the `--workspace` flag or the `DOCSCLAW_WORKSPACE` env var; the config file value takes precedence. |
103103

104104
### `tools.exec`
105105

@@ -289,6 +289,9 @@ for a complete example.
289289

290290
Workspace context loads regardless of whether `agent-config.yaml`
291291
exists. It works in both phase 1 (single-shot) and phase 2
292-
(agentic loop). The workspace directory defaults to `/workspace`;
293-
if `agent-config.yaml` specifies `tools.workspace`, that path is
294-
used instead.
292+
(agentic loop). The workspace directory is resolved with this
293+
priority:
294+
295+
1. `agent-config.yaml` `tools.workspace` (config file wins)
296+
2. `--workspace` flag or `DOCSCLAW_WORKSPACE` env var
297+
3. Default: `/workspace`

internal/cmd/serve.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,13 @@ func init() {
251251
serveCmd.Flags().String("llm-model", "", "LLM model to use")
252252
serveCmd.Flags().Int("llm-max-tokens", 4096, "Max tokens for LLM response")
253253
serveCmd.Flags().Int("llm-timeout", 45, "LLM request timeout in seconds")
254+
serveCmd.Flags().String("workspace", "",
255+
"Workspace directory path (default: /workspace)")
254256
serveCmd.Flags().String("session-db", "",
255257
"Session database backend ('memory' for in-memory, or a file path for SQLite; default: memory)")
256258

257259
_ = v.BindPFlag("config_dir", serveCmd.Flags().Lookup("config-dir"))
260+
_ = v.BindPFlag("workspace", serveCmd.Flags().Lookup("workspace"))
258261
_ = v.BindPFlag("skills_dir", serveCmd.Flags().Lookup("skills-dir"))
259262
_ = v.BindPFlag("document_service_url", serveCmd.Flags().Lookup("document-service-url"))
260263
_ = v.BindPFlag("llm.provider", serveCmd.Flags().Lookup("llm-provider"))
@@ -271,11 +274,22 @@ type Config struct {
271274
config.CommonConfig `mapstructure:",squash"`
272275
ConfigDir string `mapstructure:"config_dir"`
273276
SkillsDir string `mapstructure:"skills_dir"`
277+
Workspace string `mapstructure:"workspace"`
274278
DocumentServiceURL string `mapstructure:"document_service_url"`
275279
LLM llm.Config `mapstructure:"llm"`
276280
SessionDB string `mapstructure:"session_db"`
277281
}
278282

283+
func resolveWorkspace(cfgWorkspace, flagWorkspace string) string {
284+
if v := strings.TrimSpace(cfgWorkspace); v != "" {
285+
return v
286+
}
287+
if v := strings.TrimSpace(flagWorkspace); v != "" {
288+
return v
289+
}
290+
return defaultWorkspace
291+
}
292+
279293
// startAgent loads config from configDir and validates it.
280294
// This function is separated from runServe for testability.
281295
func startAgent(configDir string, _ llm.Provider) error {
@@ -338,6 +352,13 @@ func runServe(cmd *cobra.Command, args []string) error {
338352
return err
339353
}
340354

355+
// Resolve workspace path: agent-config.yaml > --workspace flag/env > default
356+
var cfgWorkspace string
357+
if agentCfg != nil {
358+
cfgWorkspace = agentCfg.Tools.Workspace
359+
}
360+
workspace := resolveWorkspace(cfgWorkspace, cfg.Workspace)
361+
341362
// Set up tool registry and skill loading if agent config exists
342363
var toolRegistry *tools.Registry
343364
var loopCfg tools.LoopConfig
@@ -346,10 +367,6 @@ func runServe(cmd *cobra.Command, args []string) error {
346367
if agentCfg != nil {
347368
toolRegistry = tools.NewRegistry(agentCfg.Tools.Allowed)
348369

349-
workspace := agentCfg.Tools.Workspace
350-
if workspace == "" {
351-
workspace = defaultWorkspace
352-
}
353370
if err := os.MkdirAll(workspace, 0755); err != nil {
354371
return fmt.Errorf("failed to create workspace: %w", err)
355372
}
@@ -385,10 +402,6 @@ func runServe(cmd *cobra.Command, args []string) error {
385402
slog.SetDefault(log.Logger)
386403

387404
// Load OpenClaw workspace context (works in both phase 1 and phase 2)
388-
workspace := defaultWorkspace
389-
if agentCfg != nil && agentCfg.Tools.Workspace != "" {
390-
workspace = agentCfg.Tools.Workspace
391-
}
392405
systemPrompt += loadWorkspaceContext(workspace)
393406

394407
// Load OS tool inventory and inject into system prompt

internal/cmd/workspace_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,34 @@ func TestLoadWorkspaceContextUnicodeTruncation(t *testing.T) {
167167
}
168168
}
169169

170+
func TestResolveWorkspaceConfigWins(t *testing.T) {
171+
result := resolveWorkspace("/from-config", "/from-flag")
172+
if result != "/from-config" {
173+
t.Fatalf("expected /from-config, got %q", result)
174+
}
175+
}
176+
177+
func TestResolveWorkspaceFlagFallback(t *testing.T) {
178+
result := resolveWorkspace("", "/from-flag")
179+
if result != "/from-flag" {
180+
t.Fatalf("expected /from-flag, got %q", result)
181+
}
182+
}
183+
184+
func TestResolveWorkspaceDefault(t *testing.T) {
185+
result := resolveWorkspace("", "")
186+
if result != defaultWorkspace {
187+
t.Fatalf("expected %q, got %q", defaultWorkspace, result)
188+
}
189+
}
190+
191+
func TestResolveWorkspaceWhitespaceIgnored(t *testing.T) {
192+
result := resolveWorkspace(" ", "/from-flag")
193+
if result != "/from-flag" {
194+
t.Fatalf("expected /from-flag, got %q", result)
195+
}
196+
}
197+
170198
func TestLoadWorkspaceContextNonexistentDir(t *testing.T) {
171199
result := loadWorkspaceContext("/nonexistent/path")
172200

0 commit comments

Comments
 (0)