Skip to content

Commit 0619ab1

Browse files
Merge pull request #8 from Cloud-Exit/feature/config-edit
ipc fix; claude code fix; new config edit feature
2 parents 21d2ebd + f621443 commit 0619ab1

File tree

23 files changed

+917
-530
lines changed

23 files changed

+917
-530
lines changed

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,15 @@ The first access in a session prompts for the vault password. Subsequent reads o
219219
| `codex` | OpenAI's Codex CLI | None (downloaded)|
220220
| `opencode` | OpenCode AI assistant | None (binary download) |
221221

222-
All agents are installed inside the container. Existing host config (`~/.claude`, etc.) is imported once into managed storage on first run. Use `exitbox import <agent>` (or `exitbox import all`) to re-seed from host config. Use `--workspace` to target a specific workspace.
222+
All agents are installed inside the container. Existing host config (`~/.claude`, etc.) is imported once into managed storage on first run. Use `exitbox config import <agent>` (or `exitbox config import all`) to re-seed from host config. Use `--workspace` to target a specific workspace. Use `--config`/`-c` to import a specific config file. Use `exitbox config edit <agent>` to open the agent's primary config file in your editor:
223+
224+
```bash
225+
exitbox config import codex -c config.toml # Import a config file into Codex workspace
226+
exitbox config import opencode -c opencode.json # Import OpenCode config file
227+
exitbox config import codex -c config.toml -w work # Import into specific workspace
228+
exitbox config edit claude # Edit Claude settings.json in $EDITOR
229+
exitbox config edit codex -w work # Edit Codex config.toml for 'work' workspace
230+
```
223231

224232
## Installation
225233

@@ -363,6 +371,8 @@ exitbox update # Update ExitBox to the latest version
363371
exitbox aliases # Print shell aliases for ~/.bashrc
364372
exitbox agents list # List enabled agents and their status
365373
exitbox agents config <agent> # Open agent config in $EDITOR
374+
exitbox config import <agent|all> # Import agent config from host
375+
exitbox config edit <agent> # Open agent config file in $EDITOR
366376
```
367377

368378
### Config Generation
@@ -463,7 +473,7 @@ Agents are automatically informed about vault commands and these rules via sandb
463473
- **Development stacks**: Each workspace can have its own set of development profiles (languages/tools). The setup wizard or `exitbox workspaces add` lets you pick the stack for each workspace.
464474
- **Per-project auto-detection**: Workspaces can be scoped to a directory. When you run an agent from that directory, ExitBox automatically uses the matching workspace.
465475
- **Default workspace**: Set via `exitbox setup` or `exitbox workspaces default`. Used when no directory-scoped workspace matches.
466-
- **Credential import**: When creating a workspace, you can import credentials from the host or copy them from an existing workspace. You can also import later with `exitbox import <agent> --workspace <name>`.
476+
- **Credential import**: When creating a workspace, you can import credentials from the host or copy them from an existing workspace. You can also import later with `exitbox config import <agent> --workspace <name>`.
467477

468478
#### Workspace Resolution Order
469479

@@ -571,9 +581,10 @@ exitbox run -w work claude # Use a specific workspace for this session
571581
exitbox run --full-git-support claude # Mount host .gitconfig and SSH agent
572582
exitbox run --ollama claude # Use host Ollama for local models
573583
exitbox run --memory 16g --cpus 8 claude # Custom resource limits
584+
exitbox run --version 1.0.123 claude # Pin specific agent version
574585
```
575586

576-
All flags have long forms: `-f`/`--no-firewall`, `-r`/`--read-only`, `-v`/`--verbose`, `-n`/`--no-env`, `--resume [SESSION|TOKEN]`, `--no-resume`, `--name`, `-i`/`--include-dir`, `-t`/`--tools`, `-a`/`--allow-urls`, `-u`/`--update`, `-w`/`--workspace`, `--full-git-support`, `--ollama`, `--memory`, `--cpus`.
587+
All flags have long forms: `-f`/`--no-firewall`, `-r`/`--read-only`, `-v`/`--verbose`, `-n`/`--no-env`, `--resume [SESSION|TOKEN]`, `--no-resume`, `--name`, `-i`/`--include-dir`, `-t`/`--tools`, `-a`/`--allow-urls`, `-u`/`--update`, `-w`/`--workspace`, `--full-git-support`, `--ollama`, `--memory`, `--cpus`, `--version`.
577588

578589
## Available Profiles
579590

@@ -640,6 +651,7 @@ workspaces:
640651
agents:
641652
claude:
642653
enabled: true
654+
version: "1.0.123" # pin agent version (omit for latest)
643655
codex:
644656
enabled: false
645657
opencode:
@@ -719,7 +731,7 @@ ExitBox enforces default resource limits to prevent runaway agents:
719731

720732
### What Gets Mounted
721733

722-
ExitBox uses **managed config** (import-only) with per-workspace isolation. On first run, host config is copied into the active workspace's managed directory. Host originals are never modified. Use `exitbox import <agent>` to re-seed from host config at any time, optionally with `--workspace <name>` to target a specific workspace.
734+
ExitBox uses **managed config** (import-only) with per-workspace isolation. On first run, host config is copied into the active workspace's managed directory. Host originals are never modified. Use `exitbox config import <agent>` to re-seed from host config at any time, optionally with `--workspace <name>` to target a specific workspace.
723735

724736
All managed paths follow the pattern `~/.config/exitbox/profiles/global/<workspace>/<agent>/`. For example, with workspace `default` and agent `claude`:
725737

cmd/config_cmd.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// ExitBox - Multi-Agent Container Sandbox
2+
// Copyright (C) 2026 Cloud Exit B.V.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
package cmd
18+
19+
import (
20+
"os"
21+
"os/exec"
22+
"path/filepath"
23+
"strings"
24+
25+
"github.com/cloud-exit/exitbox/internal/agent"
26+
"github.com/cloud-exit/exitbox/internal/config"
27+
"github.com/cloud-exit/exitbox/internal/profile"
28+
"github.com/cloud-exit/exitbox/internal/ui"
29+
"github.com/spf13/cobra"
30+
)
31+
32+
func newConfigCmd() *cobra.Command {
33+
cmd := &cobra.Command{
34+
Use: "config",
35+
Short: "Manage agent configuration",
36+
Long: "Import or edit agent configuration files for a workspace.",
37+
}
38+
39+
cmd.AddCommand(newConfigImportCmd())
40+
cmd.AddCommand(newConfigEditCmd())
41+
return cmd
42+
}
43+
44+
func newConfigImportCmd() *cobra.Command {
45+
var workspace string
46+
var configFile string
47+
48+
cmd := &cobra.Command{
49+
Use: "import <agent|all>",
50+
Short: "Import agent config from host",
51+
Long: `Import agent configuration and credentials from the host into a workspace.
52+
53+
By default, imports into the active workspace. Use --workspace to target
54+
a specific workspace. Use --config to import a specific config file instead
55+
of the entire host config directory.
56+
57+
Examples:
58+
exitbox config import claude Import Claude config into active workspace
59+
exitbox config import all Import all agent configs into active workspace
60+
exitbox config import claude -w work Import Claude config into 'work' workspace
61+
exitbox config import all --workspace personal Import all configs into 'personal' workspace
62+
exitbox config import codex -c config.toml Import a config file into Codex workspace
63+
exitbox config import opencode -c opencode.json Import OpenCode config file
64+
exitbox config import codex -c config.toml -w work Import into specific workspace`,
65+
Args: cobra.ExactArgs(1),
66+
Run: func(cmd *cobra.Command, args []string) {
67+
target := args[0]
68+
69+
// Validate --config flag constraints.
70+
if configFile != "" {
71+
if target == "all" {
72+
ui.Errorf("Cannot use --config with 'all'; specify a single agent")
73+
}
74+
if _, err := os.Stat(configFile); err != nil {
75+
ui.Errorf("Config file not found: %s", configFile)
76+
}
77+
}
78+
79+
var agents []string
80+
if target == "all" {
81+
agents = agent.AgentNames
82+
} else {
83+
if !agent.IsValidAgent(target) {
84+
ui.Errorf("Unknown agent: %s", target)
85+
}
86+
agents = []string{target}
87+
}
88+
89+
// Resolve target workspace.
90+
cfg := config.LoadOrDefault()
91+
workspaceName := resolveConfigWorkspace(cfg, workspace)
92+
93+
importedAny := false
94+
for _, name := range agents {
95+
a := agent.Get(name)
96+
if a == nil {
97+
continue
98+
}
99+
100+
dst := profile.WorkspaceAgentDir(workspaceName, name)
101+
if err := os.MkdirAll(dst, 0755); err != nil {
102+
ui.Warnf("Failed to create workspace dir for %s: %v", name, err)
103+
continue
104+
}
105+
106+
if configFile != "" {
107+
// Import a specific file.
108+
if err := a.ImportFile(configFile, dst); err != nil {
109+
ui.Warnf("Failed to import file into %s: %v", name, err)
110+
continue
111+
}
112+
ui.Successf("Imported %s → %s workspace '%s'",
113+
configFile, name, workspaceName)
114+
importedAny = true
115+
} else {
116+
// Import entire host config directory.
117+
src, err := a.DetectHostConfig()
118+
if err != nil {
119+
ui.Warnf("No host config found for %s", name)
120+
continue
121+
}
122+
if err := a.ImportConfig(src, dst); err != nil {
123+
ui.Warnf("Failed to import %s config: %v", name, err)
124+
continue
125+
}
126+
ui.Successf("Imported %s config from %s → workspace '%s'",
127+
name, src, workspaceName)
128+
importedAny = true
129+
}
130+
}
131+
132+
if !importedAny {
133+
if configFile != "" {
134+
ui.Warn("Config file was not imported.")
135+
} else {
136+
ui.Warn("No configs were imported.")
137+
}
138+
}
139+
},
140+
}
141+
142+
cmd.Flags().StringVarP(&workspace, "workspace", "w", "", "Target workspace (default: active workspace)")
143+
cmd.Flags().StringVarP(&configFile, "config", "c", "", "Import a specific config file (e.g. config.toml, opencode.json)")
144+
return cmd
145+
}
146+
147+
func newConfigEditCmd() *cobra.Command {
148+
var workspace string
149+
150+
cmd := &cobra.Command{
151+
Use: "edit <agent>",
152+
Short: "Open agent config file in $EDITOR",
153+
Long: `Opens the agent's primary config file in your $EDITOR (or vi).
154+
155+
Creates the file if it doesn't exist.
156+
157+
Examples:
158+
exitbox config edit claude Edit Claude settings.json
159+
exitbox config edit codex Edit Codex config.toml
160+
exitbox config edit opencode -w work Edit OpenCode config in 'work' workspace`,
161+
Args: cobra.ExactArgs(1),
162+
Run: func(cmd *cobra.Command, args []string) {
163+
name := args[0]
164+
if !agent.IsValidAgent(name) {
165+
ui.Errorf("Unknown agent: %s", name)
166+
}
167+
168+
a := agent.Get(name)
169+
if a == nil {
170+
ui.Errorf("Agent not found: %s", name)
171+
}
172+
173+
cfg := config.LoadOrDefault()
174+
workspaceName := resolveConfigWorkspace(cfg, workspace)
175+
wsDir := profile.WorkspaceAgentDir(workspaceName, name)
176+
p := a.ConfigFilePath(wsDir)
177+
178+
// Create parent dirs + empty file if it doesn't exist.
179+
if _, err := os.Stat(p); os.IsNotExist(err) {
180+
if mkErr := os.MkdirAll(filepath.Dir(p), 0755); mkErr != nil {
181+
ui.Errorf("Failed to create directory: %v", mkErr)
182+
}
183+
if wErr := os.WriteFile(p, []byte{}, 0644); wErr != nil {
184+
ui.Errorf("Failed to create config file: %v", wErr)
185+
}
186+
}
187+
188+
editor := os.Getenv("EDITOR")
189+
if editor == "" {
190+
editor = "vi"
191+
}
192+
c := exec.Command(editor, p)
193+
c.Stdin = os.Stdin
194+
c.Stdout = os.Stdout
195+
c.Stderr = os.Stderr
196+
if err := c.Run(); err != nil {
197+
ui.Errorf("Editor exited with error: %v", err)
198+
}
199+
},
200+
}
201+
202+
cmd.Flags().StringVarP(&workspace, "workspace", "w", "", "Workspace name (default: active workspace)")
203+
return cmd
204+
}
205+
206+
// resolveConfigWorkspace determines which workspace to target.
207+
func resolveConfigWorkspace(cfg *config.Config, override string) string {
208+
if override != "" {
209+
w := profile.FindWorkspace(cfg, override)
210+
if w == nil {
211+
available := profile.WorkspaceNames(cfg)
212+
if len(available) > 0 {
213+
ui.Errorf("Unknown workspace '%s'. Available: %s", override, strings.Join(available, ", "))
214+
} else {
215+
ui.Errorf("Unknown workspace '%s'. No workspaces configured. Run 'exitbox setup' first.", override)
216+
}
217+
}
218+
return w.Name
219+
}
220+
221+
// Use the active/default workspace.
222+
projectDir, _ := os.Getwd()
223+
active, _ := profile.ResolveActiveWorkspace(cfg, projectDir, "")
224+
if active != nil {
225+
return active.Workspace.Name
226+
}
227+
228+
// Fallback if no workspace exists at all.
229+
if len(cfg.Workspaces.Items) > 0 {
230+
return cfg.Workspaces.Items[0].Name
231+
}
232+
return "default"
233+
}
234+
235+
func init() {
236+
rootCmd.AddCommand(newConfigCmd())
237+
}

cmd/exitbox-allow/main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ func main() {
8686
func requestAllow(socketPath, domain string) (bool, error) {
8787
conn, err := net.Dial("unix", socketPath)
8888
if err != nil {
89-
return false, fmt.Errorf("IPC socket not available. Domain allow requests require firewall mode")
89+
// Distinguish "socket missing" from "socket exists but connect denied"
90+
// to help diagnose stale bind-mount scenarios.
91+
if _, statErr := os.Stat(socketPath); statErr == nil {
92+
return false, fmt.Errorf("IPC socket exists but connect failed (%v). The host exitbox process may have exited while this container is still running", err)
93+
}
94+
return false, fmt.Errorf("IPC socket not available (%v). Domain allow requests require firewall mode", err)
9095
}
9196
defer conn.Close()
9297

0 commit comments

Comments
 (0)