Skip to content

chore(tools): Add a bash tool that runs in a throwaway container - #1092

Merged
JeanMertz merged 2 commits into
mainfrom
bash-tool
Sep 5, 2026
Merged

chore(tools): Add a bash tool that runs in a throwaway container#1092
JeanMertz merged 2 commits into
mainfrom
bash-tool

Conversation

@JeanMertz

Copy link
Copy Markdown
Collaborator

Adds a project-local bash tool for the things the purpose-built tools don't cover: an HTTP request with curl, reshaping JSON with jq, a one-off computation. Each call starts a fresh container, runs the commands as one script under set -euo pipefail, and removes the container when the script exits. Nothing carries over between calls.

{"commands": ["curl -sS https://api.example.com/status | jq -r .version"]}

The container sees nothing of the host unless it is asked for. Workspace paths named in mounts are checked against access.fs and bind-mounted read-only under /workspace, so the tool structurally cannot become a substitute for fs_modify_file and friends. Variables named in envs are checked against access.env: a granting rule forwards them, a denying rule refuses the call, and a variable no rule mentions prompts the user before it is exposed. Values are forwarded by name (--env NAME) rather than by value, keeping secrets out of the command line and out of ps, and the prompt names the variable without ever carrying what it holds.

The default image is registry.gitlab.com/gitlab-ci-utils/curl-jq pinned at 5.0.2, carrying curl, jq, bash, git, GNU coreutils, and zip. It runs as an unprivileged user, so a command cannot install anything the image does not already ship — a real ceiling, and one the tool's description names so the assistant asks rather than trying to fetch a missing tool. Point options.image elsewhere to widen the set.

Any of container, docker, or podman will do, whichever is found on PATH first; all three take the same flags, so only the program name differs. Detection is the only part that touches the host, which is what makes the read-only mount flag and the by-name env forwarding assertable without a runtime installed. An #[ignore]d test covers the rest against a real container: that :ro holds, that bash is present in the image, and that no ENTRYPOINT swallows the bash -c invocation.

Networking is left at the runtime default. A container that can reach the network can also exfiltrate what it has been given, so this is not an isolation boundary — it is the same posture every other local tool already has, with the filesystem narrowed considerably.

Base automatically changed from access-env to main September 5, 2026 21:28
Adds a project-local `bash` tool for the things the purpose-built tools
don't cover: an HTTP request with `curl`, reshaping JSON with `jq`, a
one-off computation. Each call starts a fresh container, runs the commands
as one script under `set -euo pipefail`, and removes the container when the
script exits. Nothing carries over between calls.

```json
{"commands": ["curl -sS https://api.example.com/status | jq -r .version"]}
```

The container sees nothing of the host unless it is asked for. Workspace
paths named in `mounts` are checked against `access.fs` and bind-mounted
**read-only** under `/workspace`, so the tool structurally cannot become a
substitute for `fs_modify_file` and friends. Variables named in `envs` are
checked against `access.env`: a granting rule forwards them, a denying rule
refuses the call, and a variable no rule mentions prompts the user before it
is exposed. Values are forwarded by name (`--env NAME`) rather than by
value, keeping secrets out of the command line and out of `ps`, and the
prompt names the variable without ever carrying what it holds.

The default image is `registry.gitlab.com/gitlab-ci-utils/curl-jq` pinned at
`5.0.2`, carrying `curl`, `jq`, `bash`, `git`, GNU `coreutils`, and `zip`. It
runs as an unprivileged user, so a command cannot install anything the image
does not already ship — a real ceiling, and one the tool's description names
so the assistant asks rather than trying to fetch a missing tool. Point
`options.image` elsewhere to widen the set.

Any of `container`, `docker`, or `podman` will do, whichever is found on
`PATH` first; all three take the same flags, so only the program name
differs. Detection is the only part that touches the host, which is what
makes the read-only mount flag and the by-name env forwarding assertable
without a runtime installed. An `#[ignore]`d test covers the rest against a
real container: that `:ro` holds, that `bash` is present in the image, and
that no `ENTRYPOINT` swallows the `bash -c` invocation.

Networking is left at the runtime default. A container that can reach the
network can also exfiltrate what it has been given, so this is not an
isolation boundary — it is the same posture every other local tool already
has, with the filesystem narrowed considerably.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
…iner

Signed-off-by: Jean Mertz <git@jeanmertz.com>
@JeanMertz
JeanMertz merged commit 4742c40 into main Sep 5, 2026
21 checks passed
@JeanMertz
JeanMertz deleted the bash-tool branch September 5, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant