diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e629ab3..3bae300 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -53,6 +53,7 @@ jobs: - open-code-review - skillspector - herdr + - herdr-outside-container - officecli - strix - colibri diff --git a/README.md b/README.md index cb9efb6..f71a85b 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ This repository contains a _collection_ of Features. | open-code-review | https://github.com/alibaba/open-code-review | CLI-oriented code review tool for diffs with deterministic checks plus optional LLM review. | | SkillSpector | https://github.com/NVIDIA/SkillSpector | Standalone security scanner for local AI-agent skill/config files; useful only if you want an AI-dev-security feature. | | herdr | https://github.com/herdrdev/herdr | Fast, cross-platform CLI tool distributed as a static binary via GitHub releases. | +| herdr-outside-container | https://github.com/herdrdev/herdr | Client-only herdr CLI that controls a Herdr server on the host via a bind-mounted Unix socket, so several dev containers can share one server. | | officecli | https://github.com/iOfficeAI/OfficeCLI | AI-friendly CLI to read, edit, and automate Word, Excel, and PowerPoint files — single static binary, no Office install required. | | strix | https://github.com/usestrix/strix | Open-source AI penetration testing tool to find and fix your app's vulnerabilities. | | colibri | https://github.com/JustVugg/colibri | Run GLM-5.2 (744B MoE) on a consumer machine — pure C, zero deps, experts streamed from disk. | @@ -775,6 +776,31 @@ Running `herdr --version` inside the built container will print the version of h herdr --version ``` +### `herdr-outside-container` + +Installs `herdr` as a client-only wrapper that talks to a Herdr server running on the host through a bind-mounted socket directory. It never starts a local server or TUI. The mount is required; see [src/herdr-outside-container/README.md](src/herdr-outside-container/README.md) for setup, permissions, and security notes. Do not combine with the `herdr` feature (both install `/usr/local/bin/herdr`). + +```jsonc +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode", + "updateRemoteUserUID": true, + "features": { + "ghcr.io/jsburckhardt/devcontainer-features/herdr-outside-container:1": { + "version": "v0.9.4" + } + }, + "mounts": [ + "source=${localEnv:HOME}/.local/share/herdr-control,target=/run/herdr-host,type=bind,readonly" + ] +} +``` + +```bash +herdr --version +herdr api snapshot +``` + ### `colibri` Running `colibri` inside the built container will print the usage banner of colibri. The installed version is recorded in `/usr/local/share/colibri/VERSION`. diff --git a/src/herdr-outside-container/NOTES.md b/src/herdr-outside-container/NOTES.md new file mode 100644 index 0000000..a7c46a4 --- /dev/null +++ b/src/herdr-outside-container/NOTES.md @@ -0,0 +1,105 @@ +## Share one host Herdr server (Linux) + +This Feature installs `herdr` as a client-only wrapper around the release +binary. API commands (`agent`, `pane`, `workspace`, `api`, ...) talk to +`HERDR_SOCKET_PATH`, or `containerSocketPath` when unset. Launching the TUI or a +server, and `--session`/`--remote`/`--machine` routing, are rejected. Use the +`herdr` Feature instead if you want a local Herdr inside the container. + +Pin the same Herdr version on host and container. On the Linux Docker host, +start **one** server: + +```bash +install -d -m 700 "$HOME/.local/share/herdr-control" +HERDR_SOCKET_PATH="$HOME/.local/share/herdr-control/herdr.sock" herdr +``` + +In each repository's `.devcontainer/devcontainer.json`: + +```json +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode", + "updateRemoteUserUID": true, + "features": { + "ghcr.io/jsburckhardt/devcontainer-features/herdr-outside-container:1": { + "version": "v0.9.4" + } + }, + "mounts": [ + "source=${localEnv:HOME}/.local/share/herdr-control,target=/run/herdr-host,type=bind,readonly" + ] +} +``` + +**The mount is required.** The Feature never mounts host sockets itself, because +the host path differs per machine. Mount the socket **directory**, not the +socket file: Herdr recreates the socket on restart and a file mount would keep a +stale inode. Create the host directory before building the container. If you +change the mount `target`, set `containerSocketPath` to match. `readonly` only +prevents filesystem changes, not API writes. + +Do not combine this Feature with the `herdr` Feature in the same container: both +install `/usr/local/bin/herdr`. + +### Permissions and troubleshooting + +Herdr sockets are mode 0600, so the container user needs the **same numeric UID** +as the host socket owner (`updateRemoteUserUID` usually handles this on Linux; +rootless Docker may remap it). Never `chmod 666` the socket. + +* **No Unix socket:** start the host server, check `mounts`, rebuild the container. +* **Permission denied:** compare `id -u` with `ls -ln` on the socket. +* **Connection refused:** the socket is stale; restart the host server. +* **Unknown command:** align host and container Herdr versions. + +`herdr api snapshot` is a quick connectivity probe. + +### Agents across containers + +The socket controls terminals owned by the host server; it does not register +container processes. Run each container's agent from its own host Herdr pane +and forward the pane identity: + +```bash +devcontainer exec --workspace-folder "$HOME/repos/repo-a" \ + env HERDR_ENV=1 HERDR_PANE_ID="$HERDR_PANE_ID" \ + HERDR_SOCKET_PATH=/run/herdr-host/herdr.sock copilot +``` + +Report lifecycle state and name the agent from inside the container, then +other containers can find it and exchange terminal input/output: + +```bash +herdr pane report-agent "$HERDR_PANE_ID" --source devcontainer-copilot --agent copilot --state idle --seq 1 +herdr agent rename "$HERDR_PANE_ID" backend + +# From another container: +herdr agent list +herdr pane run "..." +herdr agent read backend --source recent --lines 100 +herdr agent wait backend --until idle --timeout 120000 +``` + +**Limitation:** `herdr agent prompt` and `agent send-keys` return +`agent_not_ready`, because the host pane's foreground process is +`devcontainer exec`, not Copilot. Use `pane run`/`pane send-text` instead; they +skip the readiness checks. Reports must use increasing `--seq` numbers. Release +the agent with `herdr pane release-agent` when it exits. See +[agent integration](https://herdr.dev/docs/add-herdr-support/). + +### Platforms + +Supported only where the Herdr server and the Docker daemon share a Linux +filesystem: native Linux, or WSL2 with both in the same distribution (not under +`/mnt/c`). Docker Desktop on macOS, native Windows (named pipes), Codespaces and +remote Docker hosts cannot mount a local socket; consider the `herdr` Feature +with Herdr's SSH [machine support](https://herdr.dev/docs/connecting-machines/). + +### Security + +Socket access is full control of **every** workspace and terminal on that +server: clients can type commands and read other agents' output, including +secrets. There is no per-agent authorization, and the wrapper is not a sandbox. +Share a server only between trusted repositories, use separate servers for +separate trust domains, and avoid approval-skipping modes such as `--yolo`. diff --git a/src/herdr-outside-container/README.md b/src/herdr-outside-container/README.md new file mode 100644 index 0000000..72dcc3f --- /dev/null +++ b/src/herdr-outside-container/README.md @@ -0,0 +1,130 @@ + +# herdr (outside of container) (herdr-outside-container) + +Client-only herdr CLI (herdrdev/herdr) that talks to a Herdr server running on the host through a bind-mounted Unix socket. Never starts a local server or TUI, so several dev containers can share one host Herdr server. + +## Example Usage + +```json +"features": { + "ghcr.io/jsburckhardt/devcontainer-features/herdr-outside-container:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| version | Version of herdr to install (e.g. 'latest' or a specific tag like 'v0.9.4'). Match the host Herdr server version. | string | latest | +| containerSocketPath | Absolute path to the mounted host API socket inside the container. Host source paths are configured in devcontainer.json mounts. | string | /run/herdr-host/herdr.sock | + +## Share one host Herdr server (Linux) + +This Feature installs `herdr` as a client-only wrapper around the release +binary. API commands (`agent`, `pane`, `workspace`, `api`, ...) talk to +`HERDR_SOCKET_PATH`, or `containerSocketPath` when unset. Launching the TUI or a +server, and `--session`/`--remote`/`--machine` routing, are rejected. Use the +`herdr` Feature instead if you want a local Herdr inside the container. + +Pin the same Herdr version on host and container. On the Linux Docker host, +start **one** server: + +```bash +install -d -m 700 "$HOME/.local/share/herdr-control" +HERDR_SOCKET_PATH="$HOME/.local/share/herdr-control/herdr.sock" herdr +``` + +In each repository's `.devcontainer/devcontainer.json`: + +```json +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode", + "updateRemoteUserUID": true, + "features": { + "ghcr.io/jsburckhardt/devcontainer-features/herdr-outside-container:1": { + "version": "v0.9.4" + } + }, + "mounts": [ + "source=${localEnv:HOME}/.local/share/herdr-control,target=/run/herdr-host,type=bind,readonly" + ] +} +``` + +**The mount is required.** The Feature never mounts host sockets itself, because +the host path differs per machine. Mount the socket **directory**, not the +socket file: Herdr recreates the socket on restart and a file mount would keep a +stale inode. Create the host directory before building the container. If you +change the mount `target`, set `containerSocketPath` to match. `readonly` only +prevents filesystem changes, not API writes. + +Do not combine this Feature with the `herdr` Feature in the same container: both +install `/usr/local/bin/herdr`. + +### Permissions and troubleshooting + +Herdr sockets are mode 0600, so the container user needs the **same numeric UID** +as the host socket owner (`updateRemoteUserUID` usually handles this on Linux; +rootless Docker may remap it). Never `chmod 666` the socket. + +* **No Unix socket:** start the host server, check `mounts`, rebuild the container. +* **Permission denied:** compare `id -u` with `ls -ln` on the socket. +* **Connection refused:** the socket is stale; restart the host server. +* **Unknown command:** align host and container Herdr versions. + +`herdr api snapshot` is a quick connectivity probe. + +### Agents across containers + +The socket controls terminals owned by the host server; it does not register +container processes. Run each container's agent from its own host Herdr pane +and forward the pane identity: + +```bash +devcontainer exec --workspace-folder "$HOME/repos/repo-a" \ + env HERDR_ENV=1 HERDR_PANE_ID="$HERDR_PANE_ID" \ + HERDR_SOCKET_PATH=/run/herdr-host/herdr.sock copilot +``` + +Report lifecycle state and name the agent from inside the container, then +other containers can find it and exchange terminal input/output: + +```bash +herdr pane report-agent "$HERDR_PANE_ID" --source devcontainer-copilot --agent copilot --state idle --seq 1 +herdr agent rename "$HERDR_PANE_ID" backend + +# From another container: +herdr agent list +herdr pane run "..." +herdr agent read backend --source recent --lines 100 +herdr agent wait backend --until idle --timeout 120000 +``` + +**Limitation:** `herdr agent prompt` and `agent send-keys` return +`agent_not_ready`, because the host pane's foreground process is +`devcontainer exec`, not Copilot. Use `pane run`/`pane send-text` instead; they +skip the readiness checks. Reports must use increasing `--seq` numbers. Release +the agent with `herdr pane release-agent` when it exits. See +[agent integration](https://herdr.dev/docs/add-herdr-support/). + +### Platforms + +Supported only where the Herdr server and the Docker daemon share a Linux +filesystem: native Linux, or WSL2 with both in the same distribution (not under +`/mnt/c`). Docker Desktop on macOS, native Windows (named pipes), Codespaces and +remote Docker hosts cannot mount a local socket; consider the `herdr` Feature +with Herdr's SSH [machine support](https://herdr.dev/docs/connecting-machines/). + +### Security + +Socket access is full control of **every** workspace and terminal on that +server: clients can type commands and read other agents' output, including +secrets. There is no per-agent authorization, and the wrapper is not a sandbox. +Share a server only between trusted repositories, use separate servers for +separate trust domains, and avoid approval-skipping modes such as `--yolo`. + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/jsburckhardt/devcontainer-features/blob/main/src/herdr-outside-container/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/herdr-outside-container/devcontainer-feature.json b/src/herdr-outside-container/devcontainer-feature.json new file mode 100644 index 0000000..a811c1a --- /dev/null +++ b/src/herdr-outside-container/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "name": "herdr (outside of container)", + "id": "herdr-outside-container", + "version": "1.0.0", + "description": "Client-only herdr CLI (herdrdev/herdr) that talks to a Herdr server running on the host through a bind-mounted Unix socket. Never starts a local server or TUI, so several dev containers can share one host Herdr server.", + "options": { + "version": { + "type": "string", + "default": "latest", + "description": "Version of herdr to install (e.g. 'latest' or a specific tag like 'v0.9.4'). Match the host Herdr server version." + }, + "containerSocketPath": { + "type": "string", + "default": "/run/herdr-host/herdr.sock", + "description": "Absolute path to the mounted host API socket inside the container. Host source paths are configured in devcontainer.json mounts." + } + } +} diff --git a/src/herdr-outside-container/herdr-client.sh b/src/herdr-outside-container/herdr-client.sh new file mode 100755 index 0000000..5a5f53f --- /dev/null +++ b/src/herdr-outside-container/herdr-client.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +fail() { + echo "Herdr outside-of-container: $*" >&2 + exit 1 +} + +case "${1:-}" in + --version|-V|--help|-h|--skill|--default-config) + [ "$#" -eq 1 ] || fail "Use a single help/version flag, or a native API subcommand." + exec /usr/local/lib/herdr/herdr "$@" + ;; + # Bare herdr/options launch the TUI; these commands start or manage a local server/install. + ""|-*|server|session|update|channel|machine) + fail "Use native API commands such as 'herdr agent list'. Local server/TUI launch is disabled; start Herdr on the host." + ;; +esac + +# Explicit session selection overrides HERDR_SOCKET_PATH in Herdr. +for arg in "$@"; do + [ "$arg" != "--" ] || break + case "$arg" in + --session|--session=*|--remote|--remote=*|--machine|--machine=*|--no-session) + fail "Session/remote routing is disabled. Select the host session by mounting its API socket." + ;; + esac +done + +IFS= read -r configured_socket < /usr/local/share/herdr/socket-path +export HERDR_SOCKET_PATH="${HERDR_SOCKET_PATH:-$configured_socket}" +[[ "$HERDR_SOCKET_PATH" = /* ]] || fail "HERDR_SOCKET_PATH must be an absolute Unix socket path." +[ -S "$HERDR_SOCKET_PATH" ] || fail "No Unix socket at '$HERDR_SOCKET_PATH'. Start the host server and bind mount its socket directory in devcontainer.json. Docker Desktop/native Windows sockets cannot be mounted this way." +[ -w "$HERDR_SOCKET_PATH" ] || fail "Socket '$HERDR_SOCKET_PATH' is not writable by UID $(id -u). Match the host UID or grant a narrow host ACL; do not chmod 666." + +exec /usr/local/lib/herdr/herdr "$@" diff --git a/src/herdr-outside-container/install.sh b/src/herdr-outside-container/install.sh new file mode 100755 index 0000000..84ebe12 --- /dev/null +++ b/src/herdr-outside-container/install.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +set -e + +# Variables +REPO_OWNER="herdrdev" +REPO_NAME="herdr" +BINARY_NAME="herdr" +FEATURE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CONTAINER_SOCKET_PATH="${CONTAINERSOCKETPATH:-/run/herdr-host/herdr.sock}" +REAL_BINARY_DIR="/usr/local/lib/herdr" +SOCKET_PATH_FILE="/usr/local/share/herdr/socket-path" + +# Validate containerSocketPath +case "$CONTAINER_SOCKET_PATH" in + /*) ;; + *) echo "containerSocketPath must be an absolute Unix socket path." >&2; exit 1 ;; +esac +if [[ "$CONTAINER_SOCKET_PATH" == *$'\n'* || "$CONTAINER_SOCKET_PATH" == *$'\r'* ]]; then + echo "containerSocketPath must not contain line breaks." >&2 + exit 1 +fi + +# Root check +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Clean up +rm -rf /var/lib/apt/lists/* + +check_packages() { + if ! dpkg -s "$@" >/dev/null 2>&1; then + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure required packages are installed +check_packages curl ca-certificates jq tar + +# Function to get the latest version from GitHub API +get_latest_version() { + LATEST_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest" + curl -sL "$LATEST_URL" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' +} + +# Resolve version +if [ -z "${VERSION:-}" ] || [ "${VERSION}" = "latest" ]; then + VERSION="$(get_latest_version)" + if [ -z "$VERSION" ]; then + echo "Failed to resolve latest version from GitHub API." >&2 + exit 1 + fi + echo "No version provided or 'latest' specified, installing the latest version: $VERSION" +else + echo "Installing version from environment variable: $VERSION" +fi + +# Determine OS +OS_RAW="$(uname -s)" +case "$OS_RAW" in + Linux) OS="linux" ;; + Darwin) OS="macos" ;; + *) echo "Unsupported OS: $OS_RAW" >&2; exit 1 ;; +esac + +# Determine architecture +ARCH_RAW="$(uname -m)" +case "$ARCH_RAW" in + x86_64|amd64) ARCH="x86_64" ;; + aarch64|arm64) ARCH="aarch64" ;; + *) echo "Unsupported architecture: $ARCH_RAW" >&2; exit 1 ;; +esac + +ASSET_NAME="${BINARY_NAME}-${OS}-${ARCH}" +DOWNLOAD_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${VERSION}/${ASSET_NAME}" + +# Download binary +TMP_DIR="$(mktemp -d)" +cd "$TMP_DIR" + +echo "Downloading $BINARY_NAME from $DOWNLOAD_URL" +curl -fsSL -o "$BINARY_NAME" "$DOWNLOAD_URL" + +# Install the real binary outside PATH; the public `herdr` is the client-only wrapper. +install -d "$REAL_BINARY_DIR" "$(dirname "$SOCKET_PATH_FILE")" +install -m 0755 "$BINARY_NAME" "$REAL_BINARY_DIR/$BINARY_NAME" +printf '%s\n' "$CONTAINER_SOCKET_PATH" > "$SOCKET_PATH_FILE" +chmod 0644 "$SOCKET_PATH_FILE" +install -m 0755 "$FEATURE_DIR/herdr-client.sh" "/usr/local/bin/$BINARY_NAME" + +# Cleanup +cd / +rm -rf "$TMP_DIR" +rm -rf /var/lib/apt/lists/* + +# Verify installation +echo "Verifying installation" +"$BINARY_NAME" --version + +echo "Herdr client-only mode installed (socket: $CONTAINER_SOCKET_PATH). Bind mount the host socket directory at runtime; see the feature README." diff --git a/test/_global/all-tools.sh b/test/_global/all-tools.sh index 726fa82..9cbcb39 100755 --- a/test/_global/all-tools.sh +++ b/test/_global/all-tools.sh @@ -42,6 +42,8 @@ check "zizmor" zizmor --version check "open-code-review" opencodereview --version check "skillspector" skillspector --version check "herdr" herdr --version +# herdr-outside-container is intentionally not in all-tools: it also installs /usr/local/bin/herdr +# (client-only wrapper) and would conflict with the herdr feature. See herdr-outside-container-specific-version. check "officecli" officecli --version check "strix" strix --version check "colibri" bash -c "command -v colibri" diff --git a/test/_global/herdr-outside-container-specific-version.sh b/test/_global/herdr-outside-container-specific-version.sh new file mode 100755 index 0000000..286b116 --- /dev/null +++ b/test/_global/herdr-outside-container-specific-version.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +check "herdr-outside-container with specific version" /bin/bash -c "herdr --version | grep '0.9.4'" + +reportResults diff --git a/test/_global/scenarios.json b/test/_global/scenarios.json index dbe3413..c3b599b 100644 --- a/test/_global/scenarios.json +++ b/test/_global/scenarios.json @@ -376,6 +376,14 @@ } } }, + "herdr-outside-container-specific-version": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "herdr-outside-container": { + "version": "v0.9.4" + } + } + }, "officecli-specific-version": { "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { diff --git a/test/herdr-outside-container/outside-of-container.sh b/test/herdr-outside-container/outside-of-container.sh new file mode 100755 index 0000000..2a2aee0 --- /dev/null +++ b/test/herdr-outside-container/outside-of-container.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +SOCKET_DIR="/tmp/herdr-external-test" +mkdir -p "$SOCKET_DIR" +chmod 700 "$SOCKET_DIR" +export HERDR_SOCKET_PATH="$SOCKET_DIR/herdr.sock" + +reject() { + local output + if output=$(herdr "$@" 2>&1); then + return 1 + fi + [[ "$output" == *"Herdr outside-of-container:"* ]] +} + +check "CLI installed without a server" herdr --version +check "missing socket diagnostic" reject agent list +check "no local TUI launch" reject +check "no local server launch" reject server +check "no session routing override" reject agent list --session other +check "no launch via version flag" reject --version --no-session +check "no server socket created" test ! -e "$HERDR_SOCKET_PATH" + +# A real external API fixture; the public CLI must never start this server. +SHELL=/bin/bash HERDR_CONFIG_PATH="$SOCKET_DIR/config.toml" /usr/local/lib/herdr/herdr server >"$SOCKET_DIR/server.log" 2>&1 & +server_pid=$! +trap 'kill "$server_pid" 2>/dev/null || true; wait "$server_pid" 2>/dev/null || true; rm -rf "$SOCKET_DIR"' EXIT +for _ in $(seq 1 100); do + [ ! -S "$HERDR_SOCKET_PATH" ] || break + sleep 0.1 +done +check "external server fixture started" test -S "$HERDR_SOCKET_PATH" + +check "no local launch via other commands" reject update +check "real external API connection" herdr api snapshot +check "discover agents through external API" herdr agent list +check "workspace status through external API" herdr workspace list +check "configured socket used without environment override" env -u HERDR_SOCKET_PATH herdr api snapshot + +# Plain shell panes stand in for host-owned panes running agents via devcontainer exec. +declare -A panes +for name in frontend backend; do + panes[$name]=$(herdr workspace create --label "$name" --cwd "$SOCKET_DIR" | jq -er '.result.root_pane.pane_id') + herdr pane report-agent "${panes[$name]}" --source test-fixture --agent copilot --state idle --seq 1 >/dev/null + herdr agent rename "${panes[$name]}" "$name" >/dev/null +done +check "shared agent identifiers" /bin/bash -c 'herdr agent list | jq -e ".result.agents | length == 2"' + +deliver() { + local name=$1 text=$2 + herdr pane run "${panes[$name]}" "echo $text" >/dev/null && + herdr pane wait-output "${panes[$name]}" --regex "(?m)^$text\$" --timeout 10000 >/dev/null && + herdr agent read "$name" --source recent --lines 30 | grep -Fqx "$text" +} +check "message delivery and response" deliver backend external-response +check "reverse message delivery and response" deliver frontend reverse-response + +# Documented limitation: the pane foreground process is not the reported agent. +check "agent prompt reports agent_not_ready" /bin/bash -c \ + '! output=$(herdr agent prompt backend "echo hi" 2>&1) && [[ "$output" == *agent_not_ready* ]]' + +herdr pane report-agent "${panes[backend]}" --source test-fixture --agent copilot --state working --seq 2 >/dev/null +check "lifecycle state wait" herdr agent wait backend --until working --timeout 10000 +check "agent release" herdr pane release-agent "${panes[backend]}" --source test-fixture --agent copilot --seq 3 + +# Removing write access emulates a socket owned by a different host UID. +chmod 400 "$HERDR_SOCKET_PATH" +# shellcheck disable=SC2016 +check "unwritable socket receives a permission diagnostic" /bin/bash -c \ + 'output=$(herdr agent list 2>&1) && exit 1; [[ "$output" == *"not writable by UID"* ]]' +chmod 600 "$HERDR_SOCKET_PATH" + +export HERDR_SOCKET_PATH="$SOCKET_DIR/unavailable.sock" +check "runtime socket override diagnostic" reject agent list + +reportResults diff --git a/test/herdr-outside-container/scenarios.json b/test/herdr-outside-container/scenarios.json new file mode 100644 index 0000000..7f73b0d --- /dev/null +++ b/test/herdr-outside-container/scenarios.json @@ -0,0 +1,11 @@ +{ + "outside-of-container": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "herdr-outside-container": { + "version": "v0.9.4", + "containerSocketPath": "/tmp/herdr-external-test/herdr.sock" + } + } + } +} diff --git a/test/herdr-outside-container/test.sh b/test/herdr-outside-container/test.sh new file mode 100755 index 0000000..3265a56 --- /dev/null +++ b/test/herdr-outside-container/test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +check "herdr-outside-container" herdr --version +check "real binary installed outside PATH" test -x /usr/local/lib/herdr/herdr +check "default socket path configured" grep -qx '/run/herdr-host/herdr.sock' /usr/local/share/herdr/socket-path +# shellcheck disable=SC2016 +check "bare herdr is rejected (no local server/TUI)" /bin/bash -c \ + 'output=$(herdr 2>&1