forked from admin/VolcanicAgenticCore
fix(docker): address review findings
Entrypoint drops the capability bounding set and sets no_new_privs. Security docs and spec no longer claim the agent can't reach the secrets; docker exec fallbacks use a clean environment. PLAYWRIGHT_ENABLED drops the nested compose default. Tests pin the Dockerfile's Playwright install step and the compose model fallback.
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_ALLOWED_USER_IDS=
|
||||
|
||||
# Model every chat uses, log level, container time zone.
|
||||
TELEGRAM_MODEL=claude-code/claude-opus-5-5
|
||||
# Model every chat uses (empty = the compose default), log level, container time zone.
|
||||
TELEGRAM_MODEL=
|
||||
LOG_LEVEL=Information
|
||||
TZ=UTC
|
||||
|
||||
@@ -29,8 +29,9 @@ DEFAULT_TIMEZONE=
|
||||
# Deny-listed variables the agent's processes may see anyway, comma separated (e.g. GH_TOKEN).
|
||||
PROCESS_ENV_ALLOW=
|
||||
|
||||
# Playwright MCP preset; empty = compose defaults (enabled if built in, headless, chromium, --no-sandbox, playwright-mcp).
|
||||
# Playwright MCP preset; empty = compose defaults (enabled, headless, chromium, --no-sandbox, playwright-mcp).
|
||||
# An override of PLAYWRIGHT_EXTRA_ARGS must keep --no-sandbox.
|
||||
# An image built with INSTALL_PLAYWRIGHT=false also needs PLAYWRIGHT_ENABLED=false.
|
||||
PLAYWRIGHT_ENABLED=
|
||||
PLAYWRIGHT_HEADLESS=
|
||||
PLAYWRIGHT_BROWSER=
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,3 +16,5 @@ artifacts/
|
||||
.claude/
|
||||
# Local compose configuration (secrets)
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
@@ -84,4 +84,5 @@ ENV Telegram__WorkspaceRoot=/data/workspaces \
|
||||
|
||||
# No USER: PID 1 (docker-init with compose's init: true) must stay root, or the agent could read every secret from
|
||||
# /proc/1/environ. setpriv drops to app and execs dotnet, so Environment.ProcessPath is the muxer the bridge needs.
|
||||
ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]
|
||||
# An empty bounding set and no_new_privs keep the agent's processes from regaining root through setuid binaries.
|
||||
ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--bounding-set=-all", "--no-new-privs", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]
|
||||
|
||||
@@ -25,8 +25,9 @@ services:
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
||||
- DEFAULT_TIMEZONE=${DEFAULT_TIMEZONE:-} # chats' default zone (scheduler spec §7); empty → TZ
|
||||
- PROCESS_ENV_ALLOW=${PROCESS_ENV_ALLOW:-} # deny-listed variables the agent may see, e.g. GH_TOKEN
|
||||
# Playwright MCP preset (MCP spec). Enabled when the image has it; the preinstalled binary, no npx download.
|
||||
- PLAYWRIGHT_ENABLED=${PLAYWRIGHT_ENABLED:-${INSTALL_PLAYWRIGHT:-true}}
|
||||
# Playwright MCP preset (MCP spec): the preinstalled binary, no npx download. Set PLAYWRIGHT_ENABLED=false for an
|
||||
# image built with INSTALL_PLAYWRIGHT=false (no nested default: Coolify parses these expressions itself).
|
||||
- PLAYWRIGHT_ENABLED=${PLAYWRIGHT_ENABLED:-true}
|
||||
- PLAYWRIGHT_HEADLESS=${PLAYWRIGHT_HEADLESS:-true} # no display in the container
|
||||
- PLAYWRIGHT_BROWSER=${PLAYWRIGHT_BROWSER:-}
|
||||
- PLAYWRIGHT_EXTRA_ARGS=${PLAYWRIGHT_EXTRA_ARGS:---no-sandbox}
|
||||
|
||||
@@ -25,9 +25,14 @@ docker compose logs -f bot
|
||||
|
||||
The bot and everything it starts run as `app` (uid/gid 1654, `HOME=/home/app`). The image's own user stays root:
|
||||
the entrypoint `setpriv` drops to `app` before it starts `dotnet`, so PID 1 (`docker-init`, from compose's
|
||||
`init: true`) is owned by root and the agent can't read the container's environment from `/proc/1/environ`.
|
||||
Don't set `user:` in the compose file: `setpriv` needs root to switch users. For the same reason `docker exec`
|
||||
opens a root shell; add `-u app` to act as the bot.
|
||||
`init: true`) is owned by root and the agent can't read the container's environment from `/proc/1/environ`. It also
|
||||
empties the capability bounding set and sets `no_new_privs`, so nothing the agent runs regains root through a setuid
|
||||
binary such as `su` or `mount`. Don't set `user:` in the compose file: `setpriv` needs root to switch users. For the
|
||||
same reason `docker exec` opens a root shell; add `-u app` to act as the bot. Any command that writes under `HOME`
|
||||
(`npm`, `npx`, `git config`, `gh auth`, `claude`) must run with `-u app`: as root it leaves root-owned files in
|
||||
`/home/app` (`HOME` is set image-wide), and the bot's `npx` and `git` then fail with `EACCES` until the next redeploy.
|
||||
An interactive `app` session should also start from a clean environment (see
|
||||
[First run and Claude login](#first-run-and-claude-login)).
|
||||
|
||||
## Build arguments
|
||||
|
||||
@@ -39,7 +44,20 @@ opens a root shell; add `-u app` to act as the bot.
|
||||
| `PLAYWRIGHT_MCP_VERSION` | `0.0.82` | Must equal the version in `PlaywrightPreset.DefaultPackage`; `PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage` fails when they differ. The compose file doesn't override it |
|
||||
|
||||
The compose file passes `INSTALL_PLAYWRIGHT` and `CLAUDE_CODE_VERSION` from `.env` (or Coolify's variables).
|
||||
Without compose: `docker build --build-arg INSTALL_PLAYWRIGHT=true -t volcanic-agentic-core .`
|
||||
Without compose:
|
||||
|
||||
```bash
|
||||
docker build --build-arg INSTALL_PLAYWRIGHT=true -t volcanic-agentic-core .
|
||||
docker run -d --init --shm-size 1g --restart unless-stopped --env-file .env \
|
||||
-e PLAYWRIGHT_ENABLED=true -e PLAYWRIGHT_HEADLESS=true -e PLAYWRIGHT_EXTRA_ARGS=--no-sandbox \
|
||||
-e PLAYWRIGHT_COMMAND=playwright-mcp \
|
||||
-v workspaces:/data/workspaces -v agent:/data/agent -v claude:/data/claude volcanic-agentic-core
|
||||
```
|
||||
|
||||
`--init` is required: without it `dotnet` is PID 1, so nothing reaps the zombies that killed `claude` trees and `bash`
|
||||
children leave behind, and PID 1 is no longer root's. `--shm-size 1g` keeps Chromium from crashing. The compose
|
||||
file's defaults (the `-e` lines, which win over `--env-file`) don't apply to a plain `docker run`, and neither does
|
||||
its `TELEGRAM_MODEL` / `LOG_LEVEL` mapping: pass `-e Telegram__Model=...` instead.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -59,14 +77,14 @@ The compose file maps them into the container.
|
||||
| `GOOGLE_CLIENT_SECRET` | — | See above | No, and redacted |
|
||||
| `DEFAULT_TIMEZONE` | the container's `TZ` | IANA zone for chats that never ran `/timezone`, e.g. `Europe/Berlin`. An unknown name stops startup | Yes |
|
||||
| `PROCESS_ENV_ALLOW` | — | Comma-separated names or globs let through despite the deny rules, e.g. `GH_TOKEN` | Yes |
|
||||
| `PLAYWRIGHT_ENABLED` | same as `INSTALL_PLAYWRIGHT` | Browser tools on or off | Yes |
|
||||
| `PLAYWRIGHT_ENABLED` | `true` | Browser tools on or off. Set it to `false` for an image built with `INSTALL_PLAYWRIGHT=false` | Yes |
|
||||
| `PLAYWRIGHT_HEADLESS` | `true` | The container has no display | Yes |
|
||||
| `PLAYWRIGHT_BROWSER` | `chromium` | Only Chromium is installed in the image | Yes |
|
||||
| `PLAYWRIGHT_EXTRA_ARGS` | `--no-sandbox` | Extra `playwright-mcp` arguments. Keep `--no-sandbox` when you override it: Chromium's sandbox needs user namespaces, which Docker's default seccomp profile denies | Yes |
|
||||
| `PLAYWRIGHT_COMMAND` | `playwright-mcp` | The preinstalled server, so the first browser call doesn't wait for an `npx` download | Yes |
|
||||
|
||||
`TELEGRAM_MODEL`, `LOG_LEVEL`, `INSTALL_PLAYWRIGHT` and `CLAUDE_CODE_VERSION` are read by the compose file only; the
|
||||
bot itself reads the names in the right-hand form.
|
||||
`TELEGRAM_MODEL` and `LOG_LEVEL` are read by the compose file only, which passes them to the bot as `Telegram__Model`
|
||||
and `Logging__LogLevel__Default`; `INSTALL_PLAYWRIGHT` and `CLAUDE_CODE_VERSION` are build arguments (see above).
|
||||
|
||||
> **Never set `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `CLAUDE_CODE_OAUTH_TOKEN`,
|
||||
> `CLAUDE_CODE_USE_BEDROCK` or `CLAUDE_CODE_USE_VERTEX`.** The Claude Code provider runs on a Claude subscription only
|
||||
@@ -145,8 +163,17 @@ process. A rejected token (`401`) exits with code 1 and invalid configuration ex
|
||||
When the login expires later, the owner gets the same prompt automatically and the paused request continues after
|
||||
the code is pasted.
|
||||
|
||||
Without the bot: `docker exec -u app -it <container> claude auth login --claudeai`. The `-u app` matters: `docker exec`
|
||||
runs as root by default, and credentials written by root can't be read by the bot.
|
||||
Without the bot:
|
||||
|
||||
```bash
|
||||
docker exec -u app -it <container> env -i HOME=/home/app \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CLAUDE_CONFIG_DIR=/data/claude TERM=xterm \
|
||||
claude auth login --claudeai
|
||||
```
|
||||
|
||||
The `-u app` matters: `docker exec` runs as root by default, and credentials written by root can't be read by the bot.
|
||||
So does `env -i`: a `docker exec` process gets the whole container environment, bot token and API keys included, and
|
||||
while it runs as `app` the agent's `bash` or a scheduled watcher can read it from `/proc/<pid>/environ`.
|
||||
|
||||
## Updating
|
||||
|
||||
@@ -173,7 +200,9 @@ docker restart <container>
|
||||
Build them against the same `Volcanic.AgenticCore.Abstractions` version as the image. See [extensions.md](extensions.md).
|
||||
|
||||
Project context files (`AGENTS.md` or `CLAUDE.md`, `PERSONA.md`, `MEMORY.md`) live in the chat's workspace directory.
|
||||
The agent can edit them, or you can with `docker exec -u app -it <container> bash`.
|
||||
The agent can edit them, or you can with `docker exec -u app -it <container> env -i HOME=/home/app
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TERM=xterm bash` (clean environment, for the reason
|
||||
given under [First run and Claude login](#first-run-and-claude-login)).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -192,9 +221,16 @@ The agent can edit them, or you can with `docker exec -u app -it <container> bas
|
||||
|
||||
The container is the isolation boundary. Inside it, the agent's `bash` runs as `app`, the same user as the bot: it
|
||||
can read the workspaces, the agent directory with every chat's sessions, and the Claude login in `/data/claude`.
|
||||
What it can't get at are the configured secrets: they are filtered out of its environment, the bot process is
|
||||
The configured secrets are kept out of its way: they are filtered out of its environment, the bot process is
|
||||
non-dumpable (its `/proc/<pid>/environ` is unreadable and it can't be attached to), PID 1 is root's so
|
||||
`/proc/1/environ` is unreadable too, the diagnostic socket is off, and secret values are redacted from tool output.
|
||||
None of this is a sandbox. Anyone on the allow-list can run any command in the container.
|
||||
`/proc/1/environ` is unreadable too, the diagnostic socket is off, setuid binaries can't raise its privileges, and
|
||||
secret values are redacted from tool output.
|
||||
|
||||
That stops secrets leaking by accident, not an agent that goes after them on purpose. `app` owns `/data/workspaces`
|
||||
and `/data/agent`, so the agent can drop a DLL into `<workspace>/.volcanic/extensions/` (loaded into the bot process,
|
||||
with its unfiltered environment, at the next `/new`) or add an MCP server whose `env` uses `${env:TELEGRAM_BOT_TOKEN}`
|
||||
to `/data/agent/settings.json`, then kill the bot so that `restart: unless-stopped` brings it back with that server.
|
||||
Treat everyone on the allow-list as able to obtain every secret you configure. None of this is a sandbox: anyone on
|
||||
the allow-list can run any command in the container.
|
||||
|
||||
Details in [telegram-bot.md](telegram-bot.md#security).
|
||||
|
||||
@@ -21,7 +21,7 @@ build and to the Claude Code provider's subscription-only rules.
|
||||
| Files | `Dockerfile`, `.dockerignore`, `docker-compose.yml`, `.env.example` at the repo root; user doc `docs/docker.md` |
|
||||
| Build | Multi-stage: `mcr.microsoft.com/dotnet/sdk:10.0-noble` → `mcr.microsoft.com/dotnet/runtime:10.0-noble` |
|
||||
| Publish | Framework-dependent `dotnet publish -c Release` of the Telegram host only. No self-contained, no AOT, no single-file: `claude` launches the bridge as `dotnet <Bridge>.dll`, so the `dotnet` muxer and a loose `Bridge.dll` + `.runtimeconfig.json` must exist |
|
||||
| Entry | `ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]`: `setpriv` (util-linux, already in the base image) drops to `app` and execs `dotnet`, so `Environment.ProcessPath` is still the muxer `BridgeLocator.FindDotnet` wants. No entrypoint script |
|
||||
| Entry | `ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--bounding-set=-all", "--no-new-privs", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]`: `setpriv` (util-linux, already in the base image) drops to `app` and execs `dotnet`, so `Environment.ProcessPath` is still the muxer `BridgeLocator.FindDotnet` wants. No entrypoint script |
|
||||
| Agent tools | `bash`, `git`, `ripgrep`, `python3` (+ `python-is-python3`, `python3-venv`), `curl`, `ca-certificates`, `tzdata`, `jq`, `less`, `procps`, `openssh-client` |
|
||||
| Node | Node.js 24 from NodeSource (`ARG NODE_MAJOR=24`); Ubuntu noble's `nodejs` is too old |
|
||||
| Claude CLI | `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}`, `ARG CLAUDE_CODE_VERSION=2.1` (newest 2.1.x at build time; set an exact version to pin). Updated by rebuilding, never in place |
|
||||
@@ -113,8 +113,9 @@ services:
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
||||
- DEFAULT_TIMEZONE=${DEFAULT_TIMEZONE:-} # chats' default zone (scheduler spec §7); empty → TZ
|
||||
- PROCESS_ENV_ALLOW=${PROCESS_ENV_ALLOW:-} # deny-listed variables the agent may see, e.g. GH_TOKEN
|
||||
# Playwright MCP preset (MCP spec). Enabled when the image has it; the preinstalled binary, no npx download.
|
||||
- PLAYWRIGHT_ENABLED=${PLAYWRIGHT_ENABLED:-${INSTALL_PLAYWRIGHT:-true}}
|
||||
# Playwright MCP preset (MCP spec): the preinstalled binary, no npx download. Set PLAYWRIGHT_ENABLED=false for an
|
||||
# image built with INSTALL_PLAYWRIGHT=false (no nested default: Coolify parses these expressions itself).
|
||||
- PLAYWRIGHT_ENABLED=${PLAYWRIGHT_ENABLED:-true}
|
||||
- PLAYWRIGHT_HEADLESS=${PLAYWRIGHT_HEADLESS:-true} # no display in the container
|
||||
- PLAYWRIGHT_BROWSER=${PLAYWRIGHT_BROWSER:-}
|
||||
- PLAYWRIGHT_EXTRA_ARGS=${PLAYWRIGHT_EXTRA_ARGS:---no-sandbox}
|
||||
@@ -159,13 +160,24 @@ volumes:
|
||||
PID 1's own `/proc/1/environ` is root-only, and the configured secrets' values are replaced by `[REDACTED]` in
|
||||
tool output (process-environment-hook spec §8–§9). The side effect is no core dumps and no
|
||||
`dotnet-dump`/`dotnet-trace` attach; `ProcessEnvironment__HardenHostProcess=false` turns it off for a debugging
|
||||
session. The container stays the real isolation boundary: none of this is a sandbox.
|
||||
session. The entrypoint also gives the bot an empty capability bounding set and `no_new_privs`, so nothing it
|
||||
spawns regains root through the base image's setuid binaries (`su`, `mount`, `passwd`, ...).
|
||||
All of this keeps secrets out of the agent's environment and tool output by accident; it does **not** stop an agent
|
||||
that goes after them on purpose. `app` owns `/data/workspaces` and `/data/agent`, extensions are discovered on every
|
||||
session creation, and MCP servers expand `${env:NAME}` from the host's unfiltered environment, so the agent can drop
|
||||
a DLL into `<workspace>/.volcanic/extensions/` (loaded into the bot at the next `/new`) or add an MCP server with
|
||||
`env: {"X": "${env:TELEGRAM_BOT_TOKEN}"}` to `/data/agent/settings.json` (loaded after it kills the same-uid bot
|
||||
and `restart: unless-stopped` brings it back). Anyone on the allow-list must be treated as able to obtain every
|
||||
secret configured here. The container stays the real isolation boundary: none of this is a sandbox (open question 5).
|
||||
- Playwright: `PLAYWRIGHT_COMMAND=playwright-mcp` runs the globally installed, version-pinned server, so the first
|
||||
browser call in a chat doesn't wait on an npx download. `PLAYWRIGHT_EXTRA_ARGS` defaults to `--no-sandbox`:
|
||||
Chromium's sandbox needs user namespaces, which Docker's default seccomp profile denies to the non-root `app` user,
|
||||
so the container is the isolation boundary. Overriding `PLAYWRIGHT_EXTRA_ARGS` must keep `--no-sandbox`.
|
||||
`PLAYWRIGHT_ENABLED` follows `INSTALL_PLAYWRIGHT` (nested default, supported by Compose v2), so an image built
|
||||
without Playwright doesn't advertise tools it can't run.
|
||||
`PLAYWRIGHT_ENABLED` defaults to `true`, so an image built with `INSTALL_PLAYWRIGHT=false` also needs
|
||||
`PLAYWRIGHT_ENABLED=false` (otherwise the `playwright` server is logged as unavailable and its tools are missing).
|
||||
It doesn't follow `INSTALL_PLAYWRIGHT` through a nested default (`${PLAYWRIGHT_ENABLED:-${INSTALL_PLAYWRIGHT:-true}}`):
|
||||
Compose v2 supports that, but Coolify parses these expressions itself to build its variable UI, and a value cut at
|
||||
the first `}` would make the host exit 2 in a restart loop.
|
||||
- The owner (first id in `TELEGRAM_ALLOWED_USER_IDS`) gets the `🟢 Online.` message and the Claude Code re-login
|
||||
prompts. The calendar has no owner special case: every chat connects its own account.
|
||||
- Coolify prefixes named volumes with the resource uuid; they survive redeploys and are only removed when the
|
||||
@@ -192,8 +204,11 @@ shows the service as running without health status; that is acceptable.
|
||||
`/data/claude`. `/claude-code-doctor` should then report logged in, `claude.ai`, bridge ok.
|
||||
4. Later expiries use the existing owner re-login flow; nothing Docker-specific.
|
||||
|
||||
Fallback without the bot: `docker exec -u app -it <container> claude auth login --claudeai` (same `CLAUDE_CONFIG_DIR`;
|
||||
`docker exec` defaults to root, whose credentials the bot couldn't read). Copying an existing `.credentials.json` into
|
||||
Fallback without the bot: `docker exec -u app -it <container> env -i HOME=/home/app
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CLAUDE_CONFIG_DIR=/data/claude TERM=xterm claude auth
|
||||
login --claudeai` (`docker exec` defaults to root, whose credentials the bot couldn't read). `env -i` matters: a
|
||||
`docker exec` process gets the whole container environment, secrets included, and an ordinary (dumpable) `app` process
|
||||
has its `/proc/<pid>/environ` readable by the agent's `bash` and watcher scripts for as long as it runs. Copying an existing `.credentials.json` into
|
||||
the `claude` volume also works but is not the documented path.
|
||||
|
||||
## Extensions and project context
|
||||
@@ -249,11 +264,16 @@ Verification record (2026-09-27, Docker Desktop 29 on Windows 11, Linux containe
|
||||
(`disabled` / `enabled (headless, chromium)`), `Speech to text`, `Using Claude Code at /usr/bin/claude, model
|
||||
claude-code/claude-opus-5-5`, `Scheduler`, `Default time zone: UTC`, `Allowed users: 1. Workspaces under
|
||||
/data/workspaces`, then `Telegram unreachable` backoff.
|
||||
- `docker compose config`: with a minimal `.env` (`INSTALL_PLAYWRIGHT=false`), `PLAYWRIGHT_ENABLED` follows it to
|
||||
`"false"`; with `.env.example`'s defaults it is `"true"`; `--no-sandbox`, `playwright-mcp`, `init: true`,
|
||||
`shm_size`, three named volumes, no ports.
|
||||
- `docker compose config`: with a minimal `.env` (`INSTALL_PLAYWRIGHT=false`, `PLAYWRIGHT_ENABLED=false`), both are
|
||||
`"false"`; with `.env.example`'s defaults both are `"true"` and `Telegram__Model` is the compose fallback;
|
||||
`--no-sandbox`, `playwright-mcp`, `init: true`, `shm_size`, three named volumes, no ports. Not yet checked on
|
||||
Coolify itself.
|
||||
- `docker compose up -d --build` with `TELEGRAM_BOT_TOKEN=123:fake`: same startup lines, then `Telegram rejected the bot
|
||||
token (401 Unauthorized)`, exit 1, and a visible `restart: unless-stopped` loop; no other warnings.
|
||||
- After review (entrypoint with `--bounding-set=-all --no-new-privs`): the bot's `/proc/<pid>/status` shows
|
||||
`CapEff`/`CapBnd` all zero and `NoNewPrivs: 1`; `su` as `app` fails; the host starts and logs the same lines; a
|
||||
headless Chromium launch (`--no-sandbox`) under the same `setpriv` flags renders a page; the `env -i` login fallback
|
||||
runs `claude --version` with only `HOME`, `PATH`, `CLAUDE_CONFIG_DIR`, `TERM` in its environment.
|
||||
|
||||
## Open questions
|
||||
|
||||
@@ -265,3 +285,7 @@ Verification record (2026-09-27, Docker Desktop 29 on Windows 11, Linux containe
|
||||
at the cost of manual bumps?
|
||||
4. Should the agent get a .NET SDK in the image (`INSTALL_DOTNET_SDK` build arg, +~700 MB) so it can build .NET
|
||||
projects in its workspace?
|
||||
5. The agent can reach the configured secrets on purpose (see the secrets bullet): it owns the directories that
|
||||
extension and MCP-server discovery read. Mitigate with a host option that turns off workspace/agent-dir extension
|
||||
and settings discovery in the container, or with root-owned, read-only `extensions/` and `settings.json` paths?
|
||||
Until then this is documented, and the allow-list is the trust boundary.
|
||||
|
||||
@@ -97,6 +97,8 @@ public class PlaywrightPresetTests
|
||||
|
||||
var match = Assert.Single(matches);
|
||||
Assert.Equal(PlaywrightPreset.PackageVersion, match.Groups["version"].Value);
|
||||
// The install step must use the ARG, not a version of its own.
|
||||
Assert.Contains("\"@playwright/mcp@${PLAYWRIGHT_MCP_VERSION}\"", dockerfile, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>A file in the repository root, found by walking up from the test output to <c>VolcanicAgenticCore.slnx</c>.</summary>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Volcanic.AgenticCore.Telegram.Tests;
|
||||
|
||||
/// <summary>Values the repo-root <c>docker-compose.yml</c> copies from the host's code.</summary>
|
||||
public sealed class DockerComposeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Compose_model_fallback_matches_TelegramOptions_DefaultModel()
|
||||
{
|
||||
var compose = File.ReadAllText(RepoFile("docker-compose.yml"));
|
||||
var matches = Regex.Matches(compose, @"Telegram__Model=\$\{TELEGRAM_MODEL:-(?<model>[^}]+)\}");
|
||||
|
||||
var match = Assert.Single(matches);
|
||||
Assert.Equal(TelegramOptions.DefaultModel, match.Groups["model"].Value);
|
||||
}
|
||||
|
||||
/// <summary>A file in the repository root, found by walking up from the test output to <c>VolcanicAgenticCore.slnx</c>.</summary>
|
||||
private static string RepoFile(string name)
|
||||
{
|
||||
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir.FullName, "VolcanicAgenticCore.slnx"))) return Path.Combine(dir.FullName, name);
|
||||
}
|
||||
throw new InvalidOperationException("VolcanicAgenticCore.slnx not found above " + AppContext.BaseDirectory);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user