lcode-cli 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/workflows/ci.yml +1 -1
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/CHANGELOG.md +24 -1
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/PKG-INFO +11 -2
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/README.md +9 -1
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/SECURITY.md +7 -2
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/configuration.md +4 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/how-it-works.md +1 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/installation.md +15 -2
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/quickstart.md +1 -1
- lcode_cli-0.5.0/docs/sandbox.md +79 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/usage.md +8 -2
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/mkdocs.yml +1 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/pyproject.toml +1 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/agent.py +18 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/cli.py +24 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/config.py +10 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/repl.py +33 -0
- lcode_cli-0.5.0/src/lcode/sandbox.py +204 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/tools.py +60 -26
- lcode_cli-0.5.0/tests/test_sandbox.py +166 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.editorconfig +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.gitignore +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/AGENTS.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/LICENSE +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/assets/logo.svg +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/assets/social-preview.png +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/bench.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/changelog.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/contributing.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/index.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/mcp.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/models.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/install.sh +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/overrides/main.html +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/bench.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/conftest.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_bench.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_config.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/tests/test_web.py +0 -0
- {lcode_cli-0.4.0 → lcode_cli-0.5.0}/uv.lock +0 -0
|
@@ -6,6 +6,27 @@ All notable changes to lcode are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- An optional sandbox for shell commands: with `lcode --sandbox` (or `sandbox = "docker"` /
|
|
14
|
+
`"podman"`), the model's commands run in a container that only sees the project folder, as your
|
|
15
|
+
user, with no network access unless you allow it (`/sandbox network on`, `sandbox_network`), no
|
|
16
|
+
Linux capabilities and a process limit. The file tools are limited to the project too. lcode builds
|
|
17
|
+
a default image with Python, Node.js, git and build tools on first use (`sandbox_image` for your
|
|
18
|
+
own). In `auto-edit` mode, sandboxed commands run without asking. If the sandbox can't start, lcode
|
|
19
|
+
stops instead of running commands without it.
|
|
20
|
+
|
|
21
|
+
## [0.4.1] - 2026-10-01
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Homebrew: `brew install nasser1941/tap/lcode` on macOS (Apple Silicon and Intel) and Linux. The
|
|
26
|
+
[tap](https://github.com/nasser1941/homebrew-tap) tests the formula on both and updates it
|
|
27
|
+
automatically for each release, about a day after it's published.
|
|
28
|
+
- Python 3.14 is supported and tested.
|
|
29
|
+
|
|
9
30
|
## [0.4.0] - 2026-10-01
|
|
10
31
|
|
|
11
32
|
### Added
|
|
@@ -165,7 +186,9 @@ First public release.
|
|
|
165
186
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
166
187
|
- One-line installer for Linux and macOS.
|
|
167
188
|
|
|
168
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
189
|
+
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.5.0...HEAD
|
|
190
|
+
[0.5.0]: https://github.com/nasser1941/lcode/compare/v0.4.1...v0.5.0
|
|
191
|
+
[0.4.1]: https://github.com/nasser1941/lcode/compare/v0.4.0...v0.4.1
|
|
169
192
|
[0.4.0]: https://github.com/nasser1941/lcode/compare/v0.3.1...v0.4.0
|
|
170
193
|
[0.3.1]: https://github.com/nasser1941/lcode/compare/v0.3.0...v0.3.1
|
|
171
194
|
[0.3.0]: https://github.com/nasser1941/lcode/compare/v0.2.2...v0.3.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
|
|
5
5
|
Project-URL: Homepage, https://nasser1941.github.io/lcode/
|
|
6
6
|
Project-URL: Documentation, https://nasser1941.github.io/lcode/
|
|
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
26
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
26
27
|
Classifier: Topic :: Software Development
|
|
27
28
|
Requires-Python: >=3.10
|
|
@@ -77,6 +78,8 @@ through [Ollama](https://ollama.com), and when it needs current information it c
|
|
|
77
78
|
real coding tasks on your machine.
|
|
78
79
|
- **Safe by default.** Every edit is shown as a diff and every command that isn't read-only needs
|
|
79
80
|
your approval. `auto-edit` and `yolo` modes when you want speed.
|
|
81
|
+
- **Sandbox (optional).** `lcode --sandbox` runs the model's shell commands in a container (Docker or
|
|
82
|
+
Podman) that only sees your project, without network access unless you allow it.
|
|
80
83
|
- **Undo.** lcode saves a checkpoint before the model changes files; `/undo` takes back the last
|
|
81
84
|
request's edits, new files and shell-command changes, without touching your git history.
|
|
82
85
|
- **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
|
|
@@ -107,6 +110,13 @@ uv tool install lcode-cli # or: pipx install lcode-cli
|
|
|
107
110
|
lcode setup
|
|
108
111
|
```
|
|
109
112
|
|
|
113
|
+
On a Mac with [Homebrew](https://brew.sh):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
brew install nasser1941/tap/lcode
|
|
117
|
+
lcode setup
|
|
118
|
+
```
|
|
119
|
+
|
|
110
120
|
See the [installation guide](https://nasser1941.github.io/lcode/installation/) for details.
|
|
111
121
|
|
|
112
122
|
## Quickstart
|
|
@@ -176,7 +186,6 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
|
|
|
176
186
|
|
|
177
187
|
See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
|
|
178
188
|
|
|
179
|
-
- [Homebrew formula](https://github.com/nasser1941/lcode/issues/8) and an [optional sandbox for shell commands](https://github.com/nasser1941/lcode/issues/9)
|
|
180
189
|
- More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
|
|
181
190
|
|
|
182
191
|
## Contributing
|
|
@@ -44,6 +44,8 @@ through [Ollama](https://ollama.com), and when it needs current information it c
|
|
|
44
44
|
real coding tasks on your machine.
|
|
45
45
|
- **Safe by default.** Every edit is shown as a diff and every command that isn't read-only needs
|
|
46
46
|
your approval. `auto-edit` and `yolo` modes when you want speed.
|
|
47
|
+
- **Sandbox (optional).** `lcode --sandbox` runs the model's shell commands in a container (Docker or
|
|
48
|
+
Podman) that only sees your project, without network access unless you allow it.
|
|
47
49
|
- **Undo.** lcode saves a checkpoint before the model changes files; `/undo` takes back the last
|
|
48
50
|
request's edits, new files and shell-command changes, without touching your git history.
|
|
49
51
|
- **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
|
|
@@ -74,6 +76,13 @@ uv tool install lcode-cli # or: pipx install lcode-cli
|
|
|
74
76
|
lcode setup
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
On a Mac with [Homebrew](https://brew.sh):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
brew install nasser1941/tap/lcode
|
|
83
|
+
lcode setup
|
|
84
|
+
```
|
|
85
|
+
|
|
77
86
|
See the [installation guide](https://nasser1941.github.io/lcode/installation/) for details.
|
|
78
87
|
|
|
79
88
|
## Quickstart
|
|
@@ -143,7 +152,6 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
|
|
|
143
152
|
|
|
144
153
|
See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
|
|
145
154
|
|
|
146
|
-
- [Homebrew formula](https://github.com/nasser1941/lcode/issues/8) and an [optional sandbox for shell commands](https://github.com/nasser1941/lcode/issues/9)
|
|
147
155
|
- More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
|
|
148
156
|
|
|
149
157
|
## Contributing
|
|
@@ -25,8 +25,13 @@ user, in your working directory**. It is not a sandbox. Keep in mind:
|
|
|
25
25
|
|
|
26
26
|
- In the default `ask` mode, every file edit and every shell command that isn't on the read-only
|
|
27
27
|
allowlist (`ls`, `cat`, `grep`, `git status`, …) is shown to you and needs your approval.
|
|
28
|
-
`auto-edit` skips approval for edits; `yolo` skips all approval — use it only
|
|
29
|
-
environments (a
|
|
28
|
+
`auto-edit` skips approval for edits; `yolo` skips all approval — use it only with the sandbox or
|
|
29
|
+
in disposable environments (a VM, a throwaway clone).
|
|
30
|
+
- The optional sandbox (`sandbox = "docker"` or `"podman"`, or `lcode --sandbox`) runs shell commands
|
|
31
|
+
in a container that sees only the project folder, as your user, without network access unless
|
|
32
|
+
allowed. The file tools are then limited to the project too. It doesn't cover web or MCP tools,
|
|
33
|
+
the project folder itself stays writable, and a container shares the host kernel; see
|
|
34
|
+
https://nasser1941.github.io/lcode/sandbox/ for its limits.
|
|
30
35
|
- Content the model reads (files, command output, search results and web pages) can contain prompt
|
|
31
36
|
injections that try to make it run harmful commands. lcode marks web content as untrusted, but
|
|
32
37
|
review commands before approving them.
|
|
@@ -25,6 +25,9 @@ lcode config path # print the file location
|
|
|
25
25
|
| `web` | `on` | Web search and page fetching: `on`, `ask` (before each search/website) or `off` |
|
|
26
26
|
| `search_backend` | `auto` | `auto`, `ollama`, `brave`, `tavily` or `searxng`; `auto` uses the first one configured |
|
|
27
27
|
| `searxng_url` | | Your SearXNG instance, e.g. `http://localhost:8888` |
|
|
28
|
+
| `sandbox` | `off` | Run shell commands in a container: `docker` or `podman` ([Sandbox](sandbox.md)) |
|
|
29
|
+
| `sandbox_image` | lcode's image | Container image for the sandbox; any image with bash and setsid |
|
|
30
|
+
| `sandbox_network` | `false` | Let commands in the sandbox use the network |
|
|
28
31
|
| `mcp_tools` | `auto` | How [MCP](mcp.md#context) tool definitions reach the model: `auto`, `direct` or `search` (on demand) |
|
|
29
32
|
| `checkpoints` | `true` | Save a checkpoint before the model changes files, so [`/undo`](usage.md#undo-and-checkpoints) can restore them |
|
|
30
33
|
|
|
@@ -50,6 +53,7 @@ Environment variables override the file, which is useful for one-off runs and CI
|
|
|
50
53
|
| `LCODE_KEEP_ALIVE` | `keep_alive` |
|
|
51
54
|
| `OLLAMA_HOST` | `ollama_host` (same variable the Ollama CLI uses) |
|
|
52
55
|
| `LCODE_WEB` | `web` |
|
|
56
|
+
| `LCODE_SANDBOX` | `sandbox` |
|
|
53
57
|
| `OLLAMA_API_KEY`, `BRAVE_API_KEY`, `TAVILY_API_KEY` | API key for that [search provider](usage.md#web-search) (never stored in the config file) |
|
|
54
58
|
| `LCODE_HOME` | where sessions and prompt history are stored |
|
|
55
59
|
|
|
@@ -77,6 +77,7 @@ the Ollama server you configure, it only contacts the web when the model searche
|
|
|
77
77
|
| `permissions.py` | Approval prompts and the read-only allowlist |
|
|
78
78
|
| `mcp/` | MCP client: stdio and HTTP transports, OAuth sign-in, server settings, the catalog, `lcode mcp` |
|
|
79
79
|
| `bench.py` | `lcode bench`: the benchmark tasks, their checks and the reports |
|
|
80
|
+
| `sandbox.py` | The optional container for shell commands |
|
|
80
81
|
| `checkpoints.py` | Snapshots before the model changes files, for `/undo` and `/rewind` |
|
|
81
82
|
| `catalog.py`, `models.toml` | Model catalog and memory estimates |
|
|
82
83
|
| `hardware.py` | NVIDIA / Apple Silicon detection |
|
|
@@ -56,6 +56,19 @@ The [installer](https://github.com/nasser1941/lcode/blob/main/install.sh) instal
|
|
|
56
56
|
[PyPI](https://pypi.org/project/lcode-cli/) with its own Python into `~/.local/bin`, checks that
|
|
57
57
|
Ollama is running, and starts `lcode setup`.
|
|
58
58
|
|
|
59
|
+
!!! tip "Homebrew"
|
|
60
|
+
|
|
61
|
+
On a Mac (or with Homebrew on Linux) you can install lcode with Homebrew instead:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
brew install nasser1941/tap/lcode
|
|
65
|
+
brew install ollama && brew services start ollama # if you don't have Ollama yet
|
|
66
|
+
lcode setup
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Upgrade with `brew upgrade lcode`. New releases reach the
|
|
70
|
+
[tap](https://github.com/nasser1941/homebrew-tap) about a day after they're published.
|
|
71
|
+
|
|
59
72
|
??? note "Prefer to do it by hand?"
|
|
60
73
|
|
|
61
74
|
lcode is published on PyPI as `lcode-cli` (the command is `lcode`). With
|
|
@@ -146,8 +159,8 @@ steps. NVIDIA GPUs work inside WSL2 with the regular Windows driver.
|
|
|
146
159
|
## Upgrade and uninstall
|
|
147
160
|
|
|
148
161
|
```bash
|
|
149
|
-
uv tool upgrade lcode-cli # upgrade (pipx: pipx upgrade lcode-cli)
|
|
150
|
-
uv tool uninstall lcode-cli # remove lcode
|
|
162
|
+
uv tool upgrade lcode-cli # upgrade (pipx: pipx upgrade lcode-cli; Homebrew: brew upgrade lcode)
|
|
163
|
+
uv tool uninstall lcode-cli # remove lcode (Homebrew: brew uninstall lcode)
|
|
151
164
|
rm -rf ~/.config/lcode ~/.local/state/lcode # remove settings and saved sessions
|
|
152
165
|
ollama rm lcode-qwen3.6-35b qwen3.6:35b-a3b-coding # remove downloaded models
|
|
153
166
|
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Sandbox
|
|
2
|
+
|
|
3
|
+
By default the model's shell commands run directly on your machine, as you. The permission prompts
|
|
4
|
+
let you check each command, but they're a guardrail, not isolation, and `yolo` mode removes even
|
|
5
|
+
that. The sandbox runs the commands in a container that can only see your project:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
lcode --sandbox # this session
|
|
9
|
+
lcode config set sandbox docker # every session (or podman)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The session's banner then shows `sandbox docker · lcode's image · no network`.
|
|
13
|
+
|
|
14
|
+
## What the container can and can't do
|
|
15
|
+
|
|
16
|
+
| | |
|
|
17
|
+
|---|---|
|
|
18
|
+
| **Sees** | Your project: the git repository you started lcode in (or the folder itself, outside a repository), mounted at the same path, read-write |
|
|
19
|
+
| **Doesn't see** | Anything else on your machine: your home folder, SSH keys, cloud credentials, other projects, the Docker socket |
|
|
20
|
+
| **Network** | None, unless you allow it (`/sandbox network on`, or `sandbox_network = true`) |
|
|
21
|
+
| **Runs as** | Your user ID, so files it creates belong to you; no Linux capabilities, no privilege escalation, at most 1,024 processes |
|
|
22
|
+
| **Lives** | For the session: started when lcode starts, removed when it exits. Containers left behind by a crash are removed the next time |
|
|
23
|
+
|
|
24
|
+
lcode's file tools (reading, editing, searching) run on your machine, but with the sandbox on they're
|
|
25
|
+
limited to the same project folder, so the model's whole world is the project.
|
|
26
|
+
|
|
27
|
+
## Approvals
|
|
28
|
+
|
|
29
|
+
With the sandbox on, `auto-edit` mode runs shell commands without asking too, since they can only
|
|
30
|
+
change the project, and [`/undo`](usage.md#undo-and-checkpoints) can take those changes back. `ask`
|
|
31
|
+
mode still asks before every command that isn't read-only. `yolo` asks for nothing, with or without
|
|
32
|
+
the sandbox.
|
|
33
|
+
|
|
34
|
+
## The image
|
|
35
|
+
|
|
36
|
+
lcode builds its default image the first time you use the sandbox (a minute or two): Python 3.12,
|
|
37
|
+
Node.js, git, build tools, ripgrep, curl and jq. Use your own image to match your project's toolchain:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
lcode config set sandbox_image node:22
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Any image with `bash` and `setsid` works. Installing packages (`pip install`, `npm install`) needs
|
|
44
|
+
the network: allow it with `/sandbox network on` for the session, or `sandbox_network = true`. The
|
|
45
|
+
model is told when a command failed because there's no network.
|
|
46
|
+
|
|
47
|
+
## Settings
|
|
48
|
+
|
|
49
|
+
| Setting | Default | |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `sandbox` | `off` | `docker` or `podman` to run commands in a container |
|
|
52
|
+
| `sandbox_image` | lcode's image | Any image with bash and setsid |
|
|
53
|
+
| `sandbox_network` | `false` | Let commands use the network |
|
|
54
|
+
|
|
55
|
+
In a session, `/sandbox` shows the sandbox and `/sandbox network on|off` changes network access (the
|
|
56
|
+
container restarts; files in the project stay). `lcode doctor` checks that Docker or Podman works.
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- **Linux:** [Docker Engine](https://docs.docker.com/engine/install/) (your user in the `docker`
|
|
61
|
+
group) or [Podman](https://podman.io/docs/installation) (rootless; lcode passes `--userns keep-id`.
|
|
62
|
+
Podman is less tested than Docker, so reports are welcome).
|
|
63
|
+
- **macOS:** Docker Desktop, [OrbStack](https://orbstack.dev) or [colima](https://github.com/abiosoft/colima).
|
|
64
|
+
Your project must be in a folder they share with containers (your home folder is, by default).
|
|
65
|
+
|
|
66
|
+
If the sandbox is on but can't start, lcode stops instead of running commands without it, and says
|
|
67
|
+
how to fix it.
|
|
68
|
+
|
|
69
|
+
## What the sandbox doesn't cover
|
|
70
|
+
|
|
71
|
+
It isolates **shell commands**. Know its limits:
|
|
72
|
+
|
|
73
|
+
- The project folder is writable: a command can still delete or damage your project's files. Use git,
|
|
74
|
+
and `/undo` for changes since the request started.
|
|
75
|
+
- With the network on, commands can reach the internet, including to send out anything in the project.
|
|
76
|
+
- [Web search and page fetching](usage.md#web-search) and [MCP tools](mcp.md) run on your machine,
|
|
77
|
+
not in the container. They still need their own approval (MCP) or can be turned off (`--no-web`).
|
|
78
|
+
- A container is not a virtual machine: it shares your machine's kernel, so a kernel vulnerability
|
|
79
|
+
could let a process escape. For untrusted code you can't afford to run, use a separate VM.
|
|
@@ -25,6 +25,7 @@ lcode mcp [catalog|add|list] # connect MCP servers: Jira, GitHub, AWS, databases
|
|
|
25
25
|
| `--no-think` | Turn off the model's reasoning: faster, less accurate |
|
|
26
26
|
| `--no-web` | No web search or page fetching in this session |
|
|
27
27
|
| `--no-mcp` | Don't start [MCP servers](mcp.md) in this session |
|
|
28
|
+
| `--sandbox` | Run shell commands in a container that only sees the project ([Sandbox](sandbox.md)) |
|
|
28
29
|
| `--show-thinking` | Print the model's reasoning as it streams |
|
|
29
30
|
| `-V, --version` | Print the version |
|
|
30
31
|
|
|
@@ -55,6 +56,7 @@ reasoning is on.
|
|
|
55
56
|
| `/undo` | Undo the file changes of the last request ([details](#undo-and-checkpoints)) |
|
|
56
57
|
| `/rewind [N]` | Go back to before request N: its files, and optionally the conversation |
|
|
57
58
|
| `/checkpoints` | List the requests that changed files, and which files |
|
|
59
|
+
| `/sandbox [network on\|off]` | The [sandbox](sandbox.md) for shell commands |
|
|
58
60
|
| `/mcp [tools\|login\|restart NAME]` | [MCP servers](mcp.md), their status and tools |
|
|
59
61
|
| `/compact [focus]` | Summarize the conversation to free context |
|
|
60
62
|
| `/context [size]` | Show how full the context window is and change its size: pick from a list with memory estimates, or give a size like `/context 128k` (`/ctx` is a shortcut) |
|
|
@@ -101,8 +103,12 @@ or ++n++. Text after ++n++ goes to the model as instructions: `n run the tests w
|
|
|
101
103
|
|
|
102
104
|
!!! warning "yolo mode"
|
|
103
105
|
|
|
104
|
-
In `yolo` mode the model can run any command as your user. Use it
|
|
105
|
-
throwaway clone, not on a machine with data you care
|
|
106
|
+
In `yolo` mode the model can run any command as your user. Use it with the
|
|
107
|
+
[sandbox](sandbox.md), in a VM or in a throwaway clone, not on a machine with data you care
|
|
108
|
+
about.
|
|
109
|
+
|
|
110
|
+
With the [sandbox](sandbox.md) on, `auto-edit` mode runs shell commands without asking too: they can
|
|
111
|
+
only reach the project, and `/undo` can take their changes back.
|
|
106
112
|
|
|
107
113
|
## Undo and checkpoints
|
|
108
114
|
|
|
@@ -24,6 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Programming Language :: Python :: 3.11",
|
|
25
25
|
"Programming Language :: Python :: 3.12",
|
|
26
26
|
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
27
28
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
29
|
"Topic :: Software Development",
|
|
29
30
|
]
|
|
@@ -26,6 +26,7 @@ from lcode.mcp import McpManager
|
|
|
26
26
|
from lcode.ollama import Ollama, OllamaError
|
|
27
27
|
from lcode.permissions import Permissions
|
|
28
28
|
from lcode.render import MarkdownStreamer
|
|
29
|
+
from lcode.sandbox import Sandbox, SandboxError, project_root
|
|
29
30
|
from lcode.tools import (
|
|
30
31
|
SCHEMAS,
|
|
31
32
|
WEB_FETCH_SCHEMA,
|
|
@@ -140,6 +141,9 @@ class Settings:
|
|
|
140
141
|
search_backend: str = "auto"
|
|
141
142
|
searxng_url: str | None = None
|
|
142
143
|
checkpoints: bool = True # snapshot files before the model changes them, for /undo
|
|
144
|
+
sandbox: str = "off" # off, docker or podman: where the model's shell commands run
|
|
145
|
+
sandbox_image: str | None = None
|
|
146
|
+
sandbox_network: bool = False
|
|
143
147
|
|
|
144
148
|
|
|
145
149
|
class Agent:
|
|
@@ -153,6 +157,11 @@ class Agent:
|
|
|
153
157
|
self.session_id = self.new_session_id()
|
|
154
158
|
self.checkpoints = Checkpoints(self.console, settings.checkpoints)
|
|
155
159
|
self.mcp: McpManager | None = None # set by the CLI when MCP servers are configured
|
|
160
|
+
self.sandbox = (
|
|
161
|
+
Sandbox(settings.sandbox, settings.sandbox_image, settings.sandbox_network, self.console)
|
|
162
|
+
if settings.sandbox != "off"
|
|
163
|
+
else None
|
|
164
|
+
)
|
|
156
165
|
self._mcp_prompt = "" # the MCP part at the end of the system prompt
|
|
157
166
|
self.session_name = ""
|
|
158
167
|
self.session_title = ""
|
|
@@ -462,6 +471,15 @@ class Agent:
|
|
|
462
471
|
finally:
|
|
463
472
|
self.checkpoints.end_turn()
|
|
464
473
|
|
|
474
|
+
def sandbox_root(self) -> Path | None:
|
|
475
|
+
"""The folder the model is limited to while the sandbox is on (None when it's off)."""
|
|
476
|
+
if not self.sandbox:
|
|
477
|
+
return None
|
|
478
|
+
try:
|
|
479
|
+
return project_root(self.cwd)
|
|
480
|
+
except SandboxError:
|
|
481
|
+
return self.cwd
|
|
482
|
+
|
|
465
483
|
def checkpoint(self) -> None:
|
|
466
484
|
"""Called before the model changes files: snapshot them once per request, for /undo."""
|
|
467
485
|
self.checkpoints.before_change(self.cwd)
|
|
@@ -321,6 +321,15 @@ def cmd_doctor(args) -> None:
|
|
|
321
321
|
except ValueError as e:
|
|
322
322
|
line("MCP", str(e), False)
|
|
323
323
|
ok = False
|
|
324
|
+
if cfg["sandbox"] == "off":
|
|
325
|
+
line("Sandbox", "off (lcode config set sandbox docker)", True)
|
|
326
|
+
else:
|
|
327
|
+
from lcode.sandbox import engine_problem
|
|
328
|
+
|
|
329
|
+
problem = engine_problem(cfg["sandbox"])
|
|
330
|
+
network = "network on" if cfg["sandbox_network"] else "no network"
|
|
331
|
+
line("Sandbox", problem or f"{cfg['sandbox']} · {network}", problem is None)
|
|
332
|
+
ok &= problem is None
|
|
324
333
|
node = shutil.which("npx")
|
|
325
334
|
line("Node.js", "found" if node else "not found (some MCP servers need npx)", True if node else None)
|
|
326
335
|
sys.exit(0 if ok else 1)
|
|
@@ -485,8 +494,20 @@ def cmd_chat(args) -> None:
|
|
|
485
494
|
search_backend=cfg["search_backend"],
|
|
486
495
|
searxng_url=cfg["searxng_url"],
|
|
487
496
|
checkpoints=cfg["checkpoints"],
|
|
497
|
+
sandbox=(cfg["sandbox"] if cfg["sandbox"] != "off" else "docker") if args.sandbox else cfg["sandbox"],
|
|
498
|
+
sandbox_image=cfg["sandbox_image"],
|
|
499
|
+
sandbox_network=cfg["sandbox_network"],
|
|
488
500
|
)
|
|
489
501
|
agent = Agent(ollama, settings, cwd, console=console)
|
|
502
|
+
if agent.sandbox:
|
|
503
|
+
# Never run commands unsandboxed when the user asked for a sandbox: stop here instead.
|
|
504
|
+
from lcode.sandbox import SandboxError
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
with console.status("Starting the sandbox…"):
|
|
508
|
+
agent.sandbox.ensure(cwd)
|
|
509
|
+
except SandboxError as e:
|
|
510
|
+
fail(f"the sandbox can't start: {e}\nTurn it off with: lcode config set sandbox off")
|
|
490
511
|
if not args.no_mcp:
|
|
491
512
|
from lcode.mcp.commands import start_session
|
|
492
513
|
|
|
@@ -496,6 +517,8 @@ def cmd_chat(args) -> None:
|
|
|
496
517
|
finally:
|
|
497
518
|
if agent.mcp:
|
|
498
519
|
agent.mcp.close()
|
|
520
|
+
if agent.sandbox:
|
|
521
|
+
agent.sandbox.stop()
|
|
499
522
|
|
|
500
523
|
|
|
501
524
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -522,6 +545,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
522
545
|
parser.add_argument("--no-think", action="store_true", help="disable model reasoning (faster, less accurate)")
|
|
523
546
|
parser.add_argument("--no-web", action="store_true", help="don't let the model search or fetch web pages")
|
|
524
547
|
parser.add_argument("--no-mcp", action="store_true", help="don't start MCP servers in this session")
|
|
548
|
+
parser.add_argument("--sandbox", action="store_true", help="run shell commands in a container (Docker or Podman)")
|
|
525
549
|
parser.add_argument("--show-thinking", action="store_true", help="print the model's reasoning as it streams")
|
|
526
550
|
parser.add_argument("-V", "--version", action="version", version=f"lcode {__version__}")
|
|
527
551
|
return parser
|
|
@@ -25,6 +25,7 @@ PERMISSION_MODES = ("ask", "auto-edit", "yolo")
|
|
|
25
25
|
WEB_MODES = ("on", "ask", "off")
|
|
26
26
|
SEARCH_BACKENDS = ("auto", "ollama", "brave", "tavily", "searxng")
|
|
27
27
|
MCP_TOOL_MODES = ("auto", "direct", "search")
|
|
28
|
+
SANDBOX_ENGINES = ("off", "docker", "podman")
|
|
28
29
|
|
|
29
30
|
# key -> (default, type, help)
|
|
30
31
|
SETTINGS: dict[str, tuple[object, type, str]] = {
|
|
@@ -39,6 +40,9 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
|
|
|
39
40
|
"search_backend": ("auto", str, "auto | ollama | brave | tavily | searxng (keys come from environment variables)"),
|
|
40
41
|
"searxng_url": (None, str, "your SearXNG instance, e.g. http://localhost:8888"),
|
|
41
42
|
"checkpoints": (True, bool, "snapshot files before the model changes them, so /undo can restore them"),
|
|
43
|
+
"sandbox": ("off", str, "run the model's shell commands in a container: off | docker | podman"),
|
|
44
|
+
"sandbox_image": (None, str, "container image for the sandbox (default: lcode's, built on first use)"),
|
|
45
|
+
"sandbox_network": (False, bool, "let commands in the sandbox use the network"),
|
|
42
46
|
"mcp_tools": ("auto", str, "how MCP tools reach the model: auto | direct | search (on demand, saves context)"),
|
|
43
47
|
}
|
|
44
48
|
ENV_OVERRIDES = {
|
|
@@ -48,6 +52,7 @@ ENV_OVERRIDES = {
|
|
|
48
52
|
"LCODE_KEEP_ALIVE": "keep_alive",
|
|
49
53
|
"OLLAMA_HOST": "ollama_host",
|
|
50
54
|
"LCODE_WEB": "web",
|
|
55
|
+
"LCODE_SANDBOX": "sandbox",
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
|
|
@@ -99,6 +104,11 @@ def coerce(key: str, value: object) -> object:
|
|
|
99
104
|
if value not in WEB_MODES:
|
|
100
105
|
raise ConfigError(f"web must be one of {', '.join(WEB_MODES)}")
|
|
101
106
|
return value
|
|
107
|
+
if key == "sandbox":
|
|
108
|
+
value = {"none": "off", "false": "off", "no": "off"}.get(str(value).lower(), str(value).lower())
|
|
109
|
+
if value not in SANDBOX_ENGINES:
|
|
110
|
+
raise ConfigError(f"sandbox must be one of {', '.join(SANDBOX_ENGINES)}")
|
|
111
|
+
return value
|
|
102
112
|
if key == "mcp_tools" and value not in MCP_TOOL_MODES:
|
|
103
113
|
raise ConfigError(f"mcp_tools must be one of {', '.join(MCP_TOOL_MODES)}")
|
|
104
114
|
if key == "search_backend" and value not in SEARCH_BACKENDS:
|
|
@@ -39,6 +39,7 @@ COMMANDS = {
|
|
|
39
39
|
"/undo": "Undo the file changes of the last request (lcode saves a checkpoint before changing files)",
|
|
40
40
|
"/rewind": "Go back to before an earlier request: its files, and optionally the conversation",
|
|
41
41
|
"/checkpoints": "List the requests that changed files, and which files",
|
|
42
|
+
"/sandbox": "Shell-command sandbox: status, or /sandbox network on|off",
|
|
42
43
|
"/mcp": "MCP servers and their tools: /mcp, /mcp tools NAME, /mcp login NAME, /mcp restart NAME",
|
|
43
44
|
"/compact": "Summarize the conversation to free context (optional: what to focus on)",
|
|
44
45
|
"/context": "Show context usage and change the window size: pick from a list, or /context 128k",
|
|
@@ -143,6 +144,11 @@ def banner(agent: Agent) -> None:
|
|
|
143
144
|
f"[dim]mode[/] {agent.perms.mode} [dim](Shift+Tab to cycle)[/]\n"
|
|
144
145
|
f"[dim]web[/] {web_status(agent)}\n"
|
|
145
146
|
+ (f"[dim]mcp[/] {escape(', '.join(agent.mcp.servers))} [dim](/mcp)[/]\n" if agent.mcp else "")
|
|
147
|
+
+ (
|
|
148
|
+
f"[dim]sandbox[/] {escape(agent.sandbox.describe())} [dim](/sandbox)[/]\n"
|
|
149
|
+
if agent.sandbox
|
|
150
|
+
else ""
|
|
151
|
+
)
|
|
146
152
|
+ "\n"
|
|
147
153
|
"[dim]/help for commands · @file to attach · Esc+Enter for a newline[/]"
|
|
148
154
|
),
|
|
@@ -240,6 +246,31 @@ def context_command(agent: Agent, arg: str, hardware: Hardware) -> None:
|
|
|
240
246
|
apply_context(agent, size, hardware)
|
|
241
247
|
|
|
242
248
|
|
|
249
|
+
def sandbox_command(agent: Agent, arg: str) -> None:
|
|
250
|
+
c, sandbox = agent.console, agent.sandbox
|
|
251
|
+
if sandbox is None:
|
|
252
|
+
c.print(
|
|
253
|
+
"The sandbox is off: shell commands run directly on this machine. Start lcode with --sandbox, or "
|
|
254
|
+
"turn it on for every session with: lcode config set sandbox docker"
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
words = arg.split()
|
|
258
|
+
if len(words) == 2 and words[0] == "network" and words[1] in ("on", "off"):
|
|
259
|
+
sandbox.network = words[1] == "on"
|
|
260
|
+
sandbox.stop() # the next command starts a container with the new setting
|
|
261
|
+
c.print(f"Network access in the sandbox is {words[1]} from the next command.")
|
|
262
|
+
return
|
|
263
|
+
if arg:
|
|
264
|
+
c.print("[yellow]Use /sandbox or /sandbox network on|off.[/]")
|
|
265
|
+
return
|
|
266
|
+
where = f" · container {sandbox.container}, mounting {sandbox.root}" if sandbox.container else ""
|
|
267
|
+
c.print(f"Sandbox: {escape(sandbox.describe())}{escape(where)}")
|
|
268
|
+
c.print(
|
|
269
|
+
"[dim]Shell commands run in the container and can only see this project; file tools are limited to it "
|
|
270
|
+
"too. In auto-edit mode, commands run without asking.[/]"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
243
274
|
def mcp_command(agent: Agent, arg: str) -> None:
|
|
244
275
|
from lcode.mcp.commands import status_table
|
|
245
276
|
from lcode.mcp.protocol import McpError
|
|
@@ -515,6 +546,8 @@ def handle_command(agent: Agent, line: str, hardware: Hardware) -> bool:
|
|
|
515
546
|
print_checkpoints(agent)
|
|
516
547
|
elif cmd == "/mcp":
|
|
517
548
|
mcp_command(agent, arg)
|
|
549
|
+
elif cmd == "/sandbox":
|
|
550
|
+
sandbox_command(agent, arg)
|
|
518
551
|
elif cmd == "/compact":
|
|
519
552
|
agent.compact(arg)
|
|
520
553
|
elif cmd in ("/context", "/ctx"):
|