bots-garden/sidekickspublic Fork
⑂ feature/m4-agents-workspace
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. d580a32 · on feature/m4-agents-workspace · k33g · 3d ago
0007-agent-workspaces.md · 53 lines · 6.1 KBmarkdown
Blame HistoryOpen raw

ADR 0007 — Agent workspaces: files and terminal

  • Status: accepted
  • Date: 2026-10-29
  • Issues: #39 (sbx, added on 2026-10-30), #27, #28, #29, #30, #31
  • Builds on: ADR 0003 (internal/workspace in the package table) and wireframe 5

Context

The brief asks to see, edit and delete the files of an agent and to open a terminal on its folder, whether the agent runs on this computer, in a Docker container or in a Docker sandbox (sbx). Without one abstraction every feature would handle three cases. The sbx CLI is not available where Sidekicks is developed, so how to run a shell and reach files in a sandbox could not be checked.

Decisions

1. One interface, two implementations in M4

internal/workspace.Workspace: Location, List, Read, Write, Remove, Changes, Shell. Paths are relative to the agent's folder, with /, and a path leaving it — .., an absolute path, a symbolic link pointing out — is refused (ErrOutside). workspace.For picks the implementation from runtime.type:

  • local: the os package, fsnotify, and a pseudo-terminal (creack/pty) running the user's $SHELL -l;
  • container: docker exec with POSIX shell commands busybox also understands (find, stat -c, cat, mv), so that small images work; workspacePath defaults to workspace (the same path, as with a bind mount);
  • sbx (issue #39, added once the owner described it): sbx exec [-i|-it] <sandbox> <command> mirrors docker exec, so containers and sandboxes share one implementation, Remote, that only uses exec, -i and -it — the folder and TERM are set inside the shell script, not with -w/-e. sbx mounts the host folder at the same path: workspacePath defaults to workspace. At first sbx was refused with ErrNotSupported — owner's choice: no untested code until the way to exec into a sandbox was known.

The same contract test suite runs on a temporary folder, in a real alpine:3.22 container (skipped when Docker or the image is missing; tests never download), and in a real sbx sandbox when SIDEKICKS_TEST_SBX names one — it passed on the owner's milo-sbx.

2. Editing safely next to a working agent

Read returns the content and a version (a hash of the content); Write replaces the file only if it still has that version, otherwise ErrConflict, and writes through a temporary file renamed over the original — the agent never reads half a file. The view offers "Take their version" or "Keep mine" on a conflict. Only text files up to 1 MB open in the editor (CodeMirror, with YAML, Markdown or JSON colouring).

3. Deleting

A local deletion moves the file to the system trash (~/.Trash on macOS, the freedesktop.org trash on Linux), so it can be put back; when the trash is on another disk, the file is deleted for good. A container has no trash: the confirmation says the deletion is final. The folder itself can never be deleted.

4. Following changes

Only the workspace shown is followed. Locally, fsnotify watches every folder except .git and node_modules, and signals once, half a second after the last event. In a container, which offers no notifications to the outside, a fingerprint of the dates and sizes is compared every two seconds. The view then reloads the open folders and the open file (or reports a conflict when it has unsaved changes).

5. Terminals

A shell runs behind a pseudo-terminal: full-screen programs work, and the window size follows the panel. Output is sent as terminal:output events grouped every 16 ms (or 32 KB), never cutting a UTF-8 character; input and resizes go through TerminalService. Closing the view ends the shell and everything it started: background jobs live in other process groups, so before hanging up Sidekicks lists the shell's descendants (ps -A -o pid=,ppid=,pgid=, the same on macOS and Linux) and signals their process groups, then kills what is left after a second; pkill -s on the session is a complement, since it does not find orphaned jobs on macOS. In a container, ending the docker exec client does not end what it started — a known Docker behaviour found while testing — so the shell records its pid and Sidekicks ends that session inside the container by reading /proc. The terminal's output is always read, into a buffer: on macOS a process cannot exit while its terminal holds unread output (found on the owner's Mac, where make check hung).

6. The view

Clicking an agent still opens its direct conversation (M3); a 📂 button on its row opens its workspace in the centre, with the tabs of wireframe 5: Files, Terminal, Log. The Files and Terminal tabs stay alive when the other is shown, so a shell survives a look at the files. The header gives the state, where the agent runs and the channels it belongs to, with Conversation, Start, Restart and Stop.

Rejected alternatives

  • Implementing sbx blindly before the owner described sbx exec: untestable here, and wrong guesses would be worse than a clear "not supported yet". Done afterwards, with the owner running the suite.
  • docker cp for files: needs a temporary file on the host for each read; docker exec … cat streams directly.
  • Offering the files to the agent through ACP (fs/*, terminal/*, issue #32): mm has its own tools; postponed.

Consequences

  • Docker must be installed for container workspaces; Sidekicks finds it in PATH or in the usual install folders, since an application opened from the Finder has a short PATH.
  • Processes killed in a container whose PID 1 is not an init stay as zombies until the container stops; a real init (docker run --init, most images' entrypoints) reaps them.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# ADR 0007 — Agent workspaces: files and terminal

- **Status**: accepted
- **Date**: 2026-10-29
- **Issues**: [#39](../../.tickets/issues/0039-workspace-et-terminal-pour-les-sandboxes-sbx.yaml) (sbx, added on 2026-10-30), [#27](../../.tickets/issues/0027-panel-agents-liste-statut-et-detail.yaml), [#28](../../.tickets/issues/0028-abstraire-le-workspace-local-sandbox-sbx-contain.yaml), [#29](../../.tickets/issues/0029-treeview-des-fichiers-du-workspace.yaml), [#30](../../.tickets/issues/0030-editer-et-supprimer-les-fichiers-du-workspace.yaml), [#31](../../.tickets/issues/0031-terminal-integre-sur-le-workspace-de-l-agent.yaml)
- **Builds on**: [ADR 0003](0003-architecture.md) (`internal/workspace` in the package table) and wireframe 5

## Context

The brief asks to see, edit and delete the files of an agent and to open a terminal on its folder, whether the agent runs on this computer, in a Docker container or in a Docker sandbox (`sbx`). Without one abstraction every feature would handle three cases. The `sbx` CLI is not available where Sidekicks is developed, so how to run a shell and reach files in a sandbox could not be checked.

## Decisions

### 1. One interface, two implementations in M4

`internal/workspace.Workspace`: `Location`, `List`, `Read`, `Write`, `Remove`, `Changes`, `Shell`. Paths are relative to the agent's folder, with `/`, and a path leaving it — `..`, an absolute path, a symbolic link pointing out — is refused (`ErrOutside`). `workspace.For` picks the implementation from `runtime.type`:

- `local`: the `os` package, fsnotify, and a pseudo-terminal (`creack/pty`) running the user's `$SHELL -l`;
- `container`: `docker exec` with POSIX shell commands busybox also understands (`find`, `stat -c`, `cat`, `mv`), so that small images work; `workspacePath` defaults to `workspace` (the same path, as with a bind mount);
- `sbx` (issue #39, added once the owner described it): `sbx exec [-i|-it] <sandbox> <command>` mirrors `docker exec`, so containers and sandboxes share one implementation, `Remote`, that only uses `exec`, `-i` and `-it` — the folder and `TERM` are set inside the shell script, not with `-w`/`-e`. sbx mounts the host folder at the same path: `workspacePath` defaults to `workspace`. At first `sbx` was refused with `ErrNotSupported` — owner's choice: no untested code until the way to exec into a sandbox was known.

The same contract test suite runs on a temporary folder, in a real `alpine:3.22` container (skipped when Docker or the image is missing; tests never download), and in a real sbx sandbox when `SIDEKICKS_TEST_SBX` names one — it passed on the owner's `milo-sbx`.

### 2. Editing safely next to a working agent

`Read` returns the content and a version (a hash of the content); `Write` replaces the file only if it still has that version, otherwise `ErrConflict`, and writes through a temporary file renamed over the original — the agent never reads half a file. The view offers "Take their version" or "Keep mine" on a conflict. Only text files up to 1 MB open in the editor (CodeMirror, with YAML, Markdown or JSON colouring).

### 3. Deleting

A local deletion moves the file to the system trash (`~/.Trash` on macOS, the freedesktop.org trash on Linux), so it can be put back; when the trash is on another disk, the file is deleted for good. A container has no trash: the confirmation says the deletion is final. The folder itself can never be deleted.

### 4. Following changes

Only the workspace shown is followed. Locally, fsnotify watches every folder except `.git` and `node_modules`, and signals once, half a second after the last event. In a container, which offers no notifications to the outside, a fingerprint of the dates and sizes is compared every two seconds. The view then reloads the open folders and the open file (or reports a conflict when it has unsaved changes).

### 5. Terminals

A shell runs behind a pseudo-terminal: full-screen programs work, and the window size follows the panel. Output is sent as `terminal:output` events grouped every 16 ms (or 32 KB), never cutting a UTF-8 character; input and resizes go through `TerminalService`. Closing the view ends the shell **and everything it started**: background jobs live in other process groups, so before hanging up Sidekicks lists the shell's descendants (`ps -A -o pid=,ppid=,pgid=`, the same on macOS and Linux) and signals their process groups, then kills what is left after a second; `pkill -s` on the session is a complement, since it does not find orphaned jobs on macOS. In a container, ending the `docker exec` client does not end what it started — a known Docker behaviour found while testing — so the shell records its pid and Sidekicks ends that session inside the container by reading `/proc`. The terminal's output is always read, into a buffer: on macOS a process cannot exit while its terminal holds unread output (found on the owner's Mac, where `make check` hung).

### 6. The view

Clicking an agent still opens its direct conversation (M3); a 📂 button on its row opens its workspace in the centre, with the tabs of wireframe 5: Files, Terminal, Log. The Files and Terminal tabs stay alive when the other is shown, so a shell survives a look at the files. The header gives the state, where the agent runs and the channels it belongs to, with Conversation, Start, Restart and Stop.

## Rejected alternatives

- **Implementing `sbx` blindly** before the owner described `sbx exec`: untestable here, and wrong guesses would be worse than a clear "not supported yet". Done afterwards, with the owner running the suite.
- **`docker cp` for files**: needs a temporary file on the host for each read; `docker exec … cat` streams directly.
- **Offering the files to the agent through ACP (`fs/*`, `terminal/*`, issue #32)**: mm has its own tools; postponed.

## Consequences

- Docker must be installed for container workspaces; Sidekicks finds it in PATH or in the usual install folders, since an application opened from the Finder has a short PATH.
- Processes killed in a container whose PID 1 is not an init stay as zombies until the container stops; a real init (`docker run --init`, most images' entrypoints) reaps them.