summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-09 04:59:39 +0000
committerClaudomator Agent <agent@claudomator>2026-03-09 04:59:39 +0000
commit67b8544b222392d8a01847e3d34559c23fd0cd12 (patch)
treeb7d27c83cac301dc9b902f3dda34d6a2a4da37d6 /internal
parentfc1459be18d4718f2c5f15325e1a1d07fb0b3a9e (diff)
api: make workspace root configurable instead of hardcoded /workspace
- Add workspaceRoot field (default "/workspace") to Server struct - Add SetWorkspaceRoot method on Server - Update handleListWorkspaces to use s.workspaceRoot - Add WorkspaceRoot field to Config with default "/workspace" - Wire cfg.WorkspaceRoot into server in serve.go - Expose --workspace-root flag on the serve command - Add TestListWorkspaces_UsesConfiguredRoot integration test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/api/server.go11
-rw-r--r--internal/api/server_test.go44
-rw-r--r--internal/cli/serve.go8
-rw-r--r--internal/config/config.go2
4 files changed, 63 insertions, 2 deletions
diff --git a/internal/api/server.go b/internal/api/server.go
index 1d87b3f..0868295 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -42,6 +42,7 @@ type Server struct {
validateCmdPath string // overrides claudeBinPath for validate; used in tests
scripts ScriptRegistry // optional; maps endpoint name → script path
workDir string // working directory injected into elaborate system prompt
+ workspaceRoot string // root directory for listing workspaces; defaults to "/workspace"
notifier notify.Notifier
apiToken string // if non-empty, required for WebSocket (and REST) connections
elaborateLimiter *ipRateLimiter // per-IP rate limiter for elaborate/validate endpoints
@@ -57,6 +58,11 @@ func (s *Server) SetNotifier(n notify.Notifier) {
s.notifier = n
}
+// SetWorkspaceRoot configures the root directory used by handleListWorkspaces.
+func (s *Server) SetWorkspaceRoot(path string) {
+ s.workspaceRoot = path
+}
+
func NewServer(store *storage.DB, pool *executor.Pool, logger *slog.Logger, claudeBinPath, geminiBinPath string) *Server {
wd, _ := os.Getwd()
s := &Server{
@@ -71,6 +77,7 @@ func NewServer(store *storage.DB, pool *executor.Pool, logger *slog.Logger, clau
claudeBinPath: claudeBinPath,
geminiBinPath: geminiBinPath,
workDir: wd,
+ workspaceRoot: "/workspace",
}
s.routes()
return s
@@ -321,7 +328,7 @@ func (s *Server) handleListWorkspaces(w http.ResponseWriter, r *http.Request) {
}
}
- entries, err := os.ReadDir("/workspace")
+ entries, err := os.ReadDir(s.workspaceRoot)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list workspaces"})
return
@@ -329,7 +336,7 @@ func (s *Server) handleListWorkspaces(w http.ResponseWriter, r *http.Request) {
var dirs []string
for _, e := range entries {
if e.IsDir() {
- dirs = append(dirs, "/workspace/"+e.Name())
+ dirs = append(dirs, s.workspaceRoot+"/"+e.Name())
}
}
writeJSON(w, http.StatusOK, dirs)
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index 8484e02..765b813 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -99,6 +99,50 @@ func (m *mockRunner) Run(_ context.Context, _ *task.Task, _ *storage.Execution)
return nil
}
+func TestListWorkspaces_UsesConfiguredRoot(t *testing.T) {
+ srv, _ := testServer(t)
+
+ root := t.TempDir()
+ for _, name := range []string{"alpha", "beta", "gamma"} {
+ if err := os.Mkdir(filepath.Join(root, name), 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // Also create a file (should be excluded from results).
+ f, err := os.Create(filepath.Join(root, "notadir.txt"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.Close()
+
+ srv.SetWorkspaceRoot(root)
+
+ req := httptest.NewRequest("GET", "/api/workspaces", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status: want 200, got %d", w.Code)
+ }
+ var dirs []string
+ if err := json.NewDecoder(w.Body).Decode(&dirs); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ want := map[string]bool{
+ root + "/alpha": true,
+ root + "/beta": true,
+ root + "/gamma": true,
+ }
+ if len(dirs) != len(want) {
+ t.Fatalf("want %d dirs, got %d: %v", len(want), len(dirs), dirs)
+ }
+ for _, d := range dirs {
+ if !want[d] {
+ t.Errorf("unexpected dir in response: %s", d)
+ }
+ }
+}
+
func TestHealthEndpoint(t *testing.T) {
srv, _ := testServer(t)
req := httptest.NewRequest("GET", "/api/health", nil)
diff --git a/internal/cli/serve.go b/internal/cli/serve.go
index 4253d56..e5bd873 100644
--- a/internal/cli/serve.go
+++ b/internal/cli/serve.go
@@ -20,16 +20,21 @@ import (
func newServeCmd() *cobra.Command {
var addr string
+ var workspaceRoot string
cmd := &cobra.Command{
Use: "serve",
Short: "Start the Claudomator API server",
RunE: func(cmd *cobra.Command, args []string) error {
+ if cmd.Flags().Changed("workspace-root") {
+ cfg.WorkspaceRoot = workspaceRoot
+ }
return serve(addr)
},
}
cmd.Flags().StringVar(&addr, "addr", ":8484", "listen address")
+ cmd.Flags().StringVar(&workspaceRoot, "workspace-root", "/workspace", "root directory for listing workspaces")
return cmd
}
@@ -77,6 +82,9 @@ func serve(addr string) error {
if cfg.WebhookURL != "" {
srv.SetNotifier(notify.NewWebhookNotifier(cfg.WebhookURL, logger))
}
+ if cfg.WorkspaceRoot != "" {
+ srv.SetWorkspaceRoot(cfg.WorkspaceRoot)
+ }
// Register scripts.
wd, _ := os.Getwd()
diff --git a/internal/config/config.go b/internal/config/config.go
index d3d9d68..daf42fe 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -17,6 +17,7 @@ type Config struct {
DefaultTimeout string `toml:"default_timeout"`
ServerAddr string `toml:"server_addr"`
WebhookURL string `toml:"webhook_url"`
+ WorkspaceRoot string `toml:"workspace_root"`
}
func Default() (*Config, error) {
@@ -37,6 +38,7 @@ func Default() (*Config, error) {
MaxConcurrent: 3,
DefaultTimeout: "15m",
ServerAddr: ":8484",
+ WorkspaceRoot: "/workspace",
}, nil
}