From 1d699c5c1dd84ced13b626b5afe56ea0f446e95b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 17:40:19 +0000 Subject: [PATCH 1/6] chore: sync package-lock.json with MIT license field Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RfjQjysq5JF7gQCJB3RQtP --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index ac1c946..207383d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "agentbox", "version": "0.1.0", + "license": "MIT", "bin": { "agentbox": "bin/agentbox.mjs" }, From 409cec0206e7d3fb882ac178d1f11cf37a7abb0f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 17:43:18 +0000 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20release=20readiness=20=E2=80=94=20?= =?UTF-8?q?scoped=20npm=20name,=20community=20files,=20CI=20matrix=20+=20p?= =?UTF-8?q?ack=20smoke=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename package to @liohtml/agentbox (bare 'agentbox' on npm is a security placeholder and cannot be claimed); bin command stays 'agentbox'. - Move tsx to dependencies so the published package runs via npx/global install without dev setup; add prepublishOnly guard + publishConfig (public access, provenance). - CI: Node 22+24 matrix, npm pack smoke test that installs the tarball and runs the CLI as an end user; narrow push triggers to main. - Add tag-triggered release workflow (npm publish --provenance). - Add SECURITY.md (threat model + private reporting), CODE_OF_CONDUCT.md, PR template, issue-template config, dependabot config, CHANGELOG.md. - Replace one-time PUBLISH.md (contained personal email) with generic RELEASING.md; README: badges + corrected npx instructions. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RfjQjysq5JF7gQCJB3RQtP --- .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/PULL_REQUEST_TEMPLATE.md | 26 ++++++++ .github/dependabot.yml | 13 ++++ .github/workflows/ci.yml | 39 ++++++++++-- .github/workflows/release.yml | 41 ++++++++++++ CHANGELOG.md | 35 +++++++++++ CODE_OF_CONDUCT.md | 38 +++++++++++ PUBLISH.md | 101 ------------------------------ README.md | 11 +++- RELEASING.md | 36 +++++++++++ SECURITY.md | 33 ++++++++++ package-lock.json | 37 ++--------- package.json | 13 +++- 13 files changed, 287 insertions(+), 141 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 PUBLISH.md create mode 100644 RELEASING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..eef0376 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/Liohtml/agentbox/security/advisories/new + about: Please report security issues privately via GitHub Security Advisories. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1a13725 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +# What & why + + + +## Scope + + + +- [ ] `src/cli/` — arg parsing, help, config, command router +- [ ] `src/docker/` — image build, container run/exec, mounts +- [ ] `src/session/` — tmux entry script, TTY sizing +- [ ] `assets/` — Dockerfile / Observer +- [ ] `src/types.ts` / `CONTRACT.md` — contract change (explain below!) +- [ ] Docs / CI / meta + +## Checklist + +- [ ] `npm run typecheck` passes +- [ ] `npm test` passes +- [ ] No build step, no new runtime dependencies (see CONTRIBUTING.md) +- [ ] Contract friction with `CONTRACT.md` / `src/types.ts` is described below (or n/a) + +## Notes + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..be9972b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + groups: + dev-dependencies: + dependency-type: development + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cbd1bb..cb52dd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: ["**"] + branches: [main] pull_request: permissions: @@ -14,16 +14,20 @@ concurrency: jobs: build: - name: typecheck & test (Node 22) + name: typecheck & test (Node ${{ matrix.node }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [22, 24] steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup Node 22 + - name: Setup Node ${{ matrix.node }} uses: actions/setup-node@v4 with: - node-version: "22" + node-version: ${{ matrix.node }} cache: npm - name: Install dependencies @@ -34,3 +38,30 @@ jobs: - name: Test run: npm test + + pack-smoke: + name: npm pack smoke test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 22 + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Pack tarball + run: npm pack --pack-destination "$RUNNER_TEMP" + + - name: Install tarball into a fresh project and run the CLI + run: | + mkdir -p "$RUNNER_TEMP/consumer" + cd "$RUNNER_TEMP/consumer" + npm init -y >/dev/null + npm install "$RUNNER_TEMP"/liohtml-agentbox-*.tgz + # The installed bin must run without any repo-local dev setup. + npx agentbox --help + npx agentbox doctor || true # doctor exits non-zero without Docker; it must still *run* + npx agentbox doctor 2>&1 | grep -q "agentbox doctor" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..088af25 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + checks: + name: typecheck & test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm test + + publish: + name: npm publish (provenance) + needs: checks + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + registry-url: "https://registry.npmjs.org" + - run: npm ci + - run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..10fd242 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project are documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project +adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Changed +- Package renamed to the scoped **`@liohtml/agentbox`** for npm publishing + (the bare name `agentbox` is an npm security placeholder). The installed + command is still `agentbox`. +- `tsx` moved from devDependencies to dependencies so the published package + runs via `npx`/global install without extra setup. +- CI: Node 22 + 24 matrix, plus an `npm pack` smoke test that installs the + tarball and runs the CLI as an end user would. + +### Added +- `SECURITY.md`, `CODE_OF_CONDUCT.md`, PR template, issue-template config, + Dependabot config, `RELEASING.md`, this changelog. +- Tag-triggered release workflow with npm provenance. + +### Removed +- `PUBLISH.md` (one-time bootstrap doc) — replaced by `RELEASING.md`. + +## [0.1.0] — 2026-07-29 + +### Added +- Initial release: `agentbox up | shell | rebuild | doctor | down`. +- Docker image (Ubuntu 24.04 + Node 22 + Python 3 + Rust + Claude Code + tsx), + non-root `agent` user, no baked-in credentials. +- Host bind mounts for workspace + Claude auth reuse (`~/.claude`, + `~/.claude.json`). +- tmux session with Claude lead (Agent Teams) + Observer dashboard window. +- Zero-dependency Observer web server on `127.0.0.1:4317`. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2ed53ca --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,38 @@ +# Code of Conduct + +This project follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +## Our pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic +status, nationality, personal appearance, race, caste, color, religion, or +sexual identity and orientation. + +## Our standards + +Examples of behavior that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes + +Examples of unacceptable behavior: + +- Sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without their explicit permission + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the maintainers via a GitHub issue (for non-sensitive matters) or +privately via [GitHub Security Advisories](https://github.com/Liohtml/agentbox/security/advisories/new). +All complaints will be reviewed and investigated promptly and fairly. + +For the full text including enforcement guidelines, see the +[Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). diff --git a/PUBLISH.md b/PUBLISH.md deleted file mode 100644 index 6d7ef2a..0000000 --- a/PUBLISH.md +++ /dev/null @@ -1,101 +0,0 @@ -# Publishing `agentbox` to GitHub - -Step-by-step to publish this directory as the **public** repo -`Liohtml/agentbox`. Run everything from the repo root: - -```bash -cd ~/sandbox/workspace/agentbox -``` - -Requirements: `git`, and the GitHub CLI `gh` (v2.87+) authenticated as -`Liohtml` (`gh auth status` should show ✓ Logged in). All these tools are -already present and authenticated on this machine. - ---- - -## 0. Prerequisite: git identity (only if unset) - -The publish creates a commit, so git needs a name and email. Check, and set if -blank: - -```bash -git config user.name # if empty, set it: -git config user.email # if empty, set it: - -git config user.name "Lio" -git config user.email "lionel.machire@googlemail.com" -``` - -(Use `--global` instead of bare flags if you want it applied for all repos.) - ---- - -## 1. Publish (primary path) - -### 1a. If this is NOT yet a git repo - -```bash -git init -b main -git add -A -git commit -m "Initial commit: agentbox CLI" -``` - -### 1b. If it IS already a git repo - -This directory is already initialized (branch `master`, with an initial -commit). Just rename the branch to `main` and capture any uncommitted work: - -```bash -git branch -M main -git add -A -git commit -m "Add project metadata: license, CI, contributing, gitignore" || echo "nothing to commit" -``` - -### 1c. Create the remote repo and push (one command) - -```bash -gh repo create Liohtml/agentbox \ - --public \ - --source=. \ - --remote=origin \ - --description "One-command, Docker-isolated agentic coding sandbox: tmux + Claude Code (Agent Teams) + web Observer." \ - --push -``` - -This creates `github.com/Liohtml/agentbox`, wires it up as the `origin` -remote, and pushes the current branch in a single step. - ---- - -## 2. Fallback (if `gh repo create --push` fails partway) - -Create the empty repo, then add the remote and push manually: - -```bash -# Create the empty public repo (no push): -gh repo create Liohtml/agentbox \ - --public \ - --description "One-command, Docker-isolated agentic coding sandbox: tmux + Claude Code (Agent Teams) + web Observer." - -# Wire up origin (only if it isn't already set): -git remote add origin https://github.com/Liohtml/agentbox.git -# If origin already exists, update it instead: -# git remote set-url origin https://github.com/Liohtml/agentbox.git - -# Push and set upstream: -git push -u origin main -``` - ---- - -## 3. Verify - -```bash -git remote -v # origin → github.com/Liohtml/agentbox -git log --oneline -1 # the commit you just pushed -gh repo view Liohtml/agentbox # repo summary in the terminal -gh repo view --web # open the repo in a browser -``` - -You should see the public repo at with -`node_modules/` excluded (via `.gitignore`) and `package-lock.json` tracked. diff --git a/README.md b/README.md index fc80395..42bf2a8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # agentbox +[![CI](https://github.com/Liohtml/agentbox/actions/workflows/ci.yml/badge.svg)](https://github.com/Liohtml/agentbox/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Node >= 22](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](package.json) + > One command spins up a Docker-isolated agentic coding sandbox — tmux + Claude Code (Agent Teams) + a live web Observer. `agentbox up` boots a disposable Ubuntu container preloaded with Node 22, Python 3, Rust, and Claude Code, bind-mounts your project **and your host Claude login** into it, and drops you into a tmux session: the Claude **lead** runs on the left, its **teammates** spawn on the right, and a live **Observer** dashboard runs in its own window — reachable from your host browser at . @@ -60,9 +64,14 @@ agentbox # 'up' is the default command — runs in the current dir You can also invoke it without linking via `npx`: ```bash -npx agentbox up # from within the cloned repo +npx . up # from within the cloned repo ``` +> Note: the package name on npm is **`@liohtml/agentbox`** (the bare name +> `agentbox` on the registry is an unrelated npm security placeholder — don't +> `npx agentbox` outside the cloned repo). Once published, use +> `npx @liohtml/agentbox up`; the installed command is still `agentbox`. + --- ## Commands diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..f1d6f5e --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,36 @@ +# Releasing + +How to cut a release of `@liohtml/agentbox`. npm publishing is intended to be +done from CI via the tag-triggered release workflow — not from a laptop. + +## Checklist + +1. **Ensure `main` is green.** CI (typecheck, tests, pack smoke test) must pass. +2. **Bump the version** in `package.json` (semver) and update the lockfile: + + ```bash + npm version --no-git-tag-version + ``` + +3. **Update `CHANGELOG.md`:** move entries from *Unreleased* into a new + `## [x.y.z] — YYYY-MM-DD` section. +4. **Commit and push** the version bump + changelog on `main` (via PR). +5. **Tag and push the tag:** + + ```bash + git tag -a vX.Y.Z -m "vX.Y.Z" + git push origin vX.Y.Z + ``` + +6. The `release.yml` workflow runs on the `v*` tag: it re-runs all checks and + publishes to npm with `--provenance --access public`. +7. **Create a GitHub Release** from the tag, pasting the changelog section. + +## npm notes + +- The package is published **scoped** as `@liohtml/agentbox` (the bare name + `agentbox` is an npm security placeholder and cannot be claimed). The + installed command is still `agentbox`. +- Publishing requires the `NPM_TOKEN` repository secret (or npm Trusted + Publishing configured for this repo), plus `id-token: write` for provenance. +- `prepublishOnly` re-runs typecheck + tests as a local safety net. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..147ab62 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,33 @@ +# Security Policy + +## Supported versions + +Only the latest release (and `main`) receive security fixes. + +## Reporting a vulnerability + +Please report vulnerabilities **privately** via +[GitHub Security Advisories](https://github.com/Liohtml/agentbox/security/advisories/new) +— do not open a public issue for security-sensitive reports. You should +receive a response within a few days. + +## Threat model — read before reporting + +agentbox is a **workbench, not a jail**. By design: + +- Your host `~/.claude` directory and `~/.claude.json` are bind-mounted + **read-write** into the container. They contain your OAuth token, the list + of project paths you've opened, and full session transcripts. Anything + running inside the box can read or modify them. Only run workloads you + trust. +- The container isolates *resources* (CPU/memory caps, `--rm`, non-root + `agent` user) — it is **not** a security boundary against malicious code. +- The Observer dashboard is unauthenticated by design; its port is published + to `127.0.0.1` **only** and must never be exposed to the LAN. + +Reports that reduce to "code inside the container can access the mounted +credentials" describe documented, intended behavior. Reports we absolutely +want: anything that breaks the documented guarantees above — e.g. the +Observer becoming reachable from non-loopback interfaces, container escape +paths agentbox introduces, injection via agentbox's own CLI/entry-script +generation, or credential exposure *beyond* the documented mounts. diff --git a/package-lock.json b/package-lock.json index 207383d..65f6e4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,21 @@ { - "name": "agentbox", + "name": "@liohtml/agentbox", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "agentbox", + "name": "@liohtml/agentbox", "version": "0.1.0", "license": "MIT", + "dependencies": { + "tsx": "^4.19.0" + }, "bin": { "agentbox": "bin/agentbox.mjs" }, "devDependencies": { "@types/node": "^22.0.0", - "tsx": "^4.19.0", "typescript": "^5.6.0" }, "engines": { @@ -27,7 +29,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -44,7 +45,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -61,7 +61,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -78,7 +77,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -95,7 +93,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -112,7 +109,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -129,7 +125,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -146,7 +141,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -163,7 +157,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -180,7 +173,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -197,7 +189,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -214,7 +205,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -231,7 +221,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -248,7 +237,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -265,7 +253,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -282,7 +269,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -299,7 +285,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -316,7 +301,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -333,7 +317,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -350,7 +333,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -367,7 +349,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -384,7 +365,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -401,7 +381,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -418,7 +397,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -435,7 +413,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -452,7 +429,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -476,7 +452,6 @@ "version": "0.28.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -518,7 +493,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -533,7 +507,6 @@ "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.28.0" diff --git a/package.json b/package.json index 222953a..1e00673 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { - "name": "agentbox", + "name": "@liohtml/agentbox", "version": "0.1.0", "description": "One-command, Docker-isolated agentic coding sandbox: tmux + Claude Code (Agent Teams) + web Observer.", "type": "module", + "publishConfig": { + "access": "public", + "provenance": true + }, "bin": { "agentbox": "bin/agentbox.mjs" }, @@ -16,7 +20,8 @@ "agentbox": "tsx src/index.ts", "doctor": "tsx src/index.ts doctor", "typecheck": "tsc --noEmit", - "test": "node --import tsx --test tests/*.test.ts" + "test": "node --import tsx --test tests/*.test.ts", + "prepublishOnly": "npm run typecheck && npm test" }, "engines": { "node": ">=22" @@ -43,8 +48,10 @@ }, "author": "Liohtml", "license": "MIT", + "dependencies": { + "tsx": "^4.19.0" + }, "devDependencies": { - "tsx": "^4.19.0", "typescript": "^5.6.0", "@types/node": "^22.0.0" } From 9738e8be84b2b2fe67e759acd71af30b93458963 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 17:57:23 +0000 Subject: [PATCH 3/6] fix: sandbox lifecycle, teammate panes, input validation, observer + launcher hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidated fixes from a code audit + upstream research: - Run the container detached with the entry script as PID 1 and attach via 'docker exec tmux attach'. Previously the tmux attach client was PID 1, so Ctrl+b d (detach) killed the lead, all teammates and the Observer, and --rm deleted the container — the opposite of the documented behavior. - Launch the lead with --teammate-mode tmux: since Claude Code v2.1.179 the default teammateMode is 'in-process', which silently disabled the split-pane team layout agentbox is built around. - Fail fast with clear errors on invalid --port/--memory/--cpus, unknown flags, stray positionals, ':' in mount paths, and a nonexistent explicitly requested workspace (no more silently created empty mounts). - rebuild refuses while the box is running (used to rebuild then silently reattach to the old container); reattach warns when tuning flags are ignored; stale stopped containers are removed before docker run. - down surfaces real 'docker rm -f' failures instead of claiming success. - ensureHostMounts: clear error when ~/.claude.json is a directory; follow symlinked configs (dotfile managers); benign EEXIST race handling. - Entry script moves to a per-user 0700 temp subdir (was a fixed world-shared /tmp path: tamper/TOCTOU + collision); never write through a pre-existing symlink. - Observer: HTML-escape project/session names (stored XSS), handle EADDRINUSE with a readable message, remain-on-exit for the observer tmux window, port sanity fallback, drop dead tail() code. - Launcher: invoke local tsx via node directly (no shell) — survives paths with spaces, removes cmd.exe metacharacter injection on Windows. - doctor: warn on host uid != 1000 (bind-mount write failures). - Dockerfile: CLAUDE_CODE_VERSION/TSX_VERSION build args for reproducible builds. - Contract & README updated; tests: 45 → 65 (new cli router suite, config validation, observer + session assertions). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RfjQjysq5JF7gQCJB3RQtP --- CHANGELOG.md | 36 ++++++++++ CONTRACT.md | 14 +++- README.md | 8 ++- ROADMAP.md | 73 +++++++++++++++++++ SECURITY.md | 7 +- assets/Dockerfile | 7 +- assets/observer.mjs | 32 +++++---- bin/agentbox.mjs | 33 ++++----- src/cli/config.ts | 57 ++++++++++++++- src/cli/index.ts | 156 ++++++++++++++++++++++++++++++++++++++--- src/docker/index.ts | 72 +++++++++++++++---- src/session/index.ts | 83 +++++++++++++++------- src/types.ts | 26 ++++--- tests/cli.test.ts | 79 +++++++++++++++++++++ tests/config.test.ts | 51 +++++++++++++- tests/observer.test.ts | 18 +++++ tests/session.test.ts | 22 +++++- 17 files changed, 673 insertions(+), 101 deletions(-) create mode 100644 ROADMAP.md create mode 100644 tests/cli.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 10fd242..f97e1b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,43 @@ adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed +- **Detaching no longer destroys the sandbox.** The container now runs + detached with the entry script as PID 1 and your terminal attaches via + `docker exec`, so `Ctrl+b d` behaves as documented (box keeps running). + Previously the tmux attach client was PID 1: detach killed every agent and + `--rm` deleted the container. +- **Teammates appear as tmux panes again.** The lead is launched with + `--teammate-mode tmux`; since Claude Code v2.1.179 the upstream default is + `in-process`, which silently disabled agentbox's split-pane layout. +- Observer: project/session names are HTML-escaped before rendering + (stored-XSS guard), the server handles bind errors (`EADDRINUSE`) with a + readable message, the observer tmux window keeps crash output visible + (`remain-on-exit`), and dead code was removed. +- CLI: invalid `--port`/`--memory`/`--cpus` values, unknown flags, stray + positionals, and `:` in mount paths fail fast with clear errors instead of + surfacing as cryptic Docker failures after a minutes-long build; a + `--workspace` typo no longer silently creates and mounts an empty + directory. +- `rebuild` refuses to run while the box is up (it used to rebuild the image + and then silently reattach to the old container); reattaching with tuning + flags now warns that they are ignored; stale stopped containers are removed + before `docker run` instead of causing a name-conflict error. +- `down` no longer reports success when `docker rm -f` actually failed. +- `~/.claude.json` edge cases: a directory there is now a clear error (was: a + broken mount), and symlinked configs (dotfile managers) are followed + correctly. +- The generated entry script lives in a per-user `0700` temp subdirectory + instead of a fixed world-shared `/tmp` path (tamper/TOCTOU + collision). +- Launcher: local `tsx` is invoked via `node` directly — no shell, so paths + with spaces work and `cmd.exe` metacharacters can't inject on Windows. +- `doctor` warns when the host uid differs from the container's `agent` + (1000), which breaks writes to the bind mounts. + ### Changed +- Dockerfile: `@anthropic-ai/claude-code` and `tsx` versions are overridable + via build args (`CLAUDE_CODE_VERSION`, `TSX_VERSION`) for reproducible + builds. - Package renamed to the scoped **`@liohtml/agentbox`** for npm publishing (the bare name `agentbox` is an npm security placeholder). The installed command is still `agentbox`. diff --git a/CONTRACT.md b/CONTRACT.md index 2ee3166..622b244 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -66,7 +66,9 @@ Defaults (`DEFAULTS` in types): - `claudeDir` default = `~/.claude`, `claudeJson` default = `~/.claude.json` - `dockerfile` = `assets/Dockerfile`, `buildContext` = `assets/` - `observerAsset` = `assets/observer.mjs` -- `entryScriptPath` = a host temp path (e.g. `os.tmpdir()/agentbox-entry.sh`) +- `entryScriptPath` = a host temp path in a per-user 0700 subdir + (`os.tmpdir()/agentbox-/entry.sh`) — never a fixed path in the shared + temp dir (tamper/TOCTOU on multi-user hosts) --- @@ -117,8 +119,16 @@ export const session: SessionModule; // see src/types.ts `main`. 4. **Launch the entry script via `bash /agentbox/entry.sh`** so a missing execute bit never blocks startup. -5. Set `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in the `main` window. +5. Set `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in the `main` window, and + launch the lead with `--teammate-mode tmux`: since Claude Code v2.1.179 + the default teammateMode is `in-process`, which silently disables the + split-pane layout. 6. Read the real TTY size via `stty size` so tmux isn't stuck at 80×24. +7. **The tmux attach client must NEVER be container PID 1.** The container + runs detached (`docker run -d`); the entry script sets the session up and + idles while it exists, and clients attach via `docker exec … tmux attach`. + Otherwise `C-b d` (detach) exits PID 1, kills every agent, and `--rm` + deletes the container. --- diff --git a/README.md b/README.md index 42bf2a8..4e61def 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ WORKSPACE=~/code/myapp MEM=8g CPUS=6 agentbox up --port 5000 1. **Builds the image** from `assets/Dockerfile` if it's missing — Ubuntu 24.04 with Node 22, Python 3, Rust (rustup), git, tmux, and Claude Code + `tsx` installed globally. It runs as the non-root user **`agent`** and never contains credentials. 2. **Prepares the host bind mounts** (below), touching `~/.claude.json` first so Docker mounts a *file*, not a directory. 3. **Generates a tmux entry script** sized to your real terminal (read via `stty size`, so tmux isn't stuck at 80×24). -4. **Runs the container** (`docker run -it`, with `--memory`/`--cpus` limits and the Observer port published), launching the entry script via **`bash /agentbox/entry.sh`** — using `bash` so a missing execute bit can never block startup. If the container is already running, it reattaches instead of starting a second session. +4. **Runs the container detached** (`docker run -d --rm`, with `--memory`/`--cpus` limits and the Observer port published), launching the entry script via **`bash /agentbox/entry.sh`** — using `bash` so a missing execute bit can never block startup. The entry script sets the tmux session up and stays alive as PID 1; agentbox then attaches your terminal via `docker exec … tmux attach`. Detaching (`Ctrl+b d`) only ends that client — the box, the lead, and all teammates keep running. If the container is already running, `up` just reattaches. ### Bind mounts (host → container) @@ -138,7 +138,7 @@ WORKSPACE=~/code/myapp MEM=8g CPUS=6 agentbox up --port 5000 Two windows: -- **`main`** — the Claude Code lead, launched with `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. Teammates the lead spawns appear as panes here. +- **`main`** — the Claude Code lead, launched with `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` and `--teammate-mode tmux` (required since Claude Code v2.1.179, where the default became `in-process` — without it teammates would not appear as panes). Teammates the lead spawns appear as panes here. - **`observer`** — the Observer web server (its own window, never a pane in `main`). | Keys | Action | @@ -163,7 +163,7 @@ The `main` window starts the Claude Code lead with Agent Teams enabled. Just tal agentbox isolates **resources** (CPU/memory caps, a throwaway `--rm` container, a non-root `agent` user) — but it is **not a security sandbox for untrusted code**. Two things to be clear about: -- **Your host Claude credentials are mounted read-write.** `~/.claude` and `~/.claude.json` carry your OAuth token, the list of every project path you've opened, and full session transcripts under `~/.claude/projects`. Anything running inside the box can read or modify them. Only run workloads you trust. +- **Your host Claude credentials are mounted read-write.** `~/.claude` and `~/.claude.json` carry your OAuth token, the list of every project path you've opened, and full session transcripts under `~/.claude/projects`. Anything running inside the box can read or modify them. That includes `~/.claude/settings.json` and hooks — code in the box could plant a hook that your **host** Claude Code executes later, i.e. the mount is a container-to-host escape channel for untrusted workloads. Only run workloads you trust. (Narrowing this mount to just credentials is on the [roadmap](ROADMAP.md).) - **The container is a workbench, not a jail.** Treat it as a clean, disposable, resource-capped place for *your own* agentic workflows — not as a barrier against malicious code. (The Observer is published to `127.0.0.1` only, so its unauthenticated dashboard never leaks to the LAN.) --- @@ -215,6 +215,8 @@ Baked into the implementation and the [contract](CONTRACT.md): 3. **The Observer lives in its own window**, never a pane in `main` — Claude's tmux teammate mode only splits the lead's window into panes if that window is otherwise empty. 4. **The entry script is launched via `bash`**, so a missing execute bit can't block startup. 5. **TTY size is read with `stty size`** so tmux opens at your real terminal dimensions instead of the 80×24 default. +6. **The tmux attach client is never container PID 1.** The container runs detached and your terminal attaches via `docker exec`, so `Ctrl+b d` detaches without tearing down the sandbox. (If the attach client were PID 1, a detach would kill every agent and `--rm` would delete the container.) +7. **The lead is launched with `--teammate-mode tmux`** — since Claude Code v2.1.179 the default is `in-process`, which would silently disable the split-pane team layout agentbox is built around. --- diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..9b885ce --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,73 @@ +# Roadmap + +Prioritized improvement backlog, curated from ecosystem research +(comparable sandboxes: textcortex/claude-code-sandbox, claude-pod, ai-pod, +claudeman; Anthropic's devcontainer reference; Agent Teams docs, 2026-07). +Ordered by impact-to-effort. PRs welcome — please open an issue first for +anything marked M/L effort. + +## Now (small effort, high impact) + +1. **Force split-pane teammate mode** *(S, high — bug-level)* — + Since Claude Code v2.1.179 the default `teammateMode` is `in-process`, + so teammates no longer appear as tmux panes — agentbox's signature + layout. Launch the lead with `--teammate-mode tmux`. +2. **`--yolo` flag** *(S, high)* — opt-in + `--dangerously-skip-permissions` for the lead (teammates inherit it). A + disposable, resource-capped container is exactly the environment this + flag exists for; teammate permission prompts otherwise bubble up to the + lead and multiply friction. +3. **Persistent tool-cache volumes** *(S, high)* — named Docker volumes + for `~/.npm`, `~/.cargo/registry`, and pip's cache. The box stays + `--rm`-disposable; only caches survive, killing the cold-start tax. +4. **Git identity + `gh` auth forwarding** *(S, high)* — mount + `~/.gitconfig` (read-only) and forward `gh` auth so agents can commit + and open PRs. Same philosophy as Claude auth reuse: borrow host + identity, never store it. + +## Next (medium effort) + +5. **Named boxes / one container per project** *(M, high)* — derive + container name, tmux session, and Observer port from the workspace path + (plus `--name` override) so two projects never share/steal one box. + Removes the biggest documented footgun. +6. **Credential isolation** *(M, high)* — mount only + `.credentials.json` + a minimal `.claude.json` into a per-box dir + instead of all of `~/.claude` (which carries transcripts and every + project path). Also fixes the Observer showing other projects' + sessions. +7. **Per-project config** *(M, high)* — optional `agentbox.json` (extra + apt packages, setup commands, env, mounts, resource defaults) or a + custom Dockerfile `FROM agentbox:latest`. Explicitly *not* full + devcontainer.json compatibility. +8. **Session git-branch safety net** *(M, med-high)* — opt-in: create + `agentbox/` branch on `up`, print a diff summary on `down`. +9. **Podman support** *(M, med)* — detect `podman` when `docker` is + absent; rootless-by-default strengthens the isolation story. +10. **Headless mode: `agentbox run ""`** *(M, med)* — run + `claude -p` in the box without tmux/TTY, stream output, exit with + Claude's status. Makes agentbox scriptable for cron/CI. + +## Later + +11. **Opt-in egress firewall (`--restrict-network`)** *(M-L, med-high)* — + port Anthropic's devcontainer `init-firewall.sh` (default-deny outbound, + allowlist api.anthropic.com/npm/GitHub; `NET_ADMIN` for init, then drop). +12. **Observer: Agent-Teams hook feed** *(M, med)* — register + `TeammateIdle`/`TaskCreated`/`TaskCompleted` hooks that POST to the + Observer for an authoritative live task board instead of inferred + JSONL activity. +13. **ARM64 / Apple Silicon polish** *(S, med)* — `doctor` reports host + arch, `up` warns on image/host arch mismatch (qemu emulation is slow), + `--platform` pass-through. +14. **`doctor`/`down` polish** *(S, low-med)* — sweep orphaned tmux + sessions on `down`; auto-pick the next free Observer port on `up`. +15. **`--gpus` pass-through** *(S, low)* — thin mapping to + `docker run --gpus` (Linux/NVIDIA), consistent with `--memory`/`--cpus`. + +## Non-goals + +- Full devcontainer.json compatibility (enterprise-bloat path). +- Being a security jail — see SECURITY.md; the egress firewall is + hardening, not a sandbox guarantee. +- Baking credentials or per-user state into the image. Ever. diff --git a/SECURITY.md b/SECURITY.md index 147ab62..822e35b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,8 +18,11 @@ agentbox is a **workbench, not a jail**. By design: - Your host `~/.claude` directory and `~/.claude.json` are bind-mounted **read-write** into the container. They contain your OAuth token, the list of project paths you've opened, and full session transcripts. Anything - running inside the box can read or modify them. Only run workloads you - trust. + running inside the box can read or modify them — including + `~/.claude/settings.json` and hooks, which your **host** Claude Code + executes. In other words, for untrusted workloads this mount is a + container-to-host escape channel. Only run workloads you trust. + (Narrowing the mount to just credentials is on the roadmap.) - The container isolates *resources* (CPU/memory caps, `--rm`, non-root `agent` user) — it is **not** a security boundary against malicious code. - The Observer dashboard is unauthenticated by design; its port is published diff --git a/assets/Dockerfile b/assets/Dockerfile index 04e45ef..77f980a 100644 --- a/assets/Dockerfile +++ b/assets/Dockerfile @@ -34,8 +34,11 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && node --version && npm --version # ── Global npm tools: Claude Code + tsx ─────────────────────────────────────── -# Pinned to global prefix; no auth baked in. -RUN npm install -g @anthropic-ai/claude-code tsx \ +# No auth baked in. Versions overridable for reproducible builds: +# docker build --build-arg CLAUDE_CODE_VERSION=2.1.180 --build-arg TSX_VERSION=4.19.2 … +ARG CLAUDE_CODE_VERSION=latest +ARG TSX_VERSION=latest +RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" "tsx@${TSX_VERSION}" \ && npm cache clean --force # ── Non-root user `agent` ───────────────────────────────────────────────────── diff --git a/assets/observer.mjs b/assets/observer.mjs index acf518a..8885f2e 100644 --- a/assets/observer.mjs +++ b/assets/observer.mjs @@ -10,10 +10,11 @@ * It surfaces live agent-team activity by tailing the Claude projects dir. */ import { createServer } from "node:http"; -import { readdirSync, readFileSync, statSync, existsSync } from "node:fs"; +import { readdirSync, statSync, existsSync } from "node:fs"; import { join } from "node:path"; -const PORT = Number(process.env.OBSERVER_PORT || 4317); +const RAW_PORT = Number(process.env.OBSERVER_PORT || 4317); +const PORT = Number.isInteger(RAW_PORT) && RAW_PORT > 0 && RAW_PORT <= 65535 ? RAW_PORT : 4317; const HOST = "0.0.0.0"; const PROJECTS_DIR = process.env.CLAUDE_PROJECTS_DIR || "/home/agent/.claude/projects"; @@ -49,15 +50,6 @@ function collectSessions() { return out.slice(0, 50); } -function tail(file, n) { - try { - const lines = readFileSync(file, "utf8").trim().split("\n"); - return lines.slice(-n); - } catch { - return []; - } -} - const PAGE = ` agentbox observer @@ -81,6 +73,10 @@ const PAGE = ` loading…