maesttro 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docker/Dockerfile CHANGED
@@ -1,10 +1,43 @@
1
+ # git is built from source: Debian ships 2.47 and the sandbox wants a recent
2
+ # git. Builder matches node:22-slim's Debian so the runtime libs line up.
3
+ FROM debian:bookworm-slim AS git-build
4
+ ARG GIT_VERSION=2.55.0
5
+ ARG GIT_SHA256=457fdb04dc8728e007d4688695e6912e6f680727920f2a40bf11eacc17505357
6
+ RUN apt-get update \
7
+ && apt-get install -y --no-install-recommends ca-certificates curl xz-utils \
8
+ build-essential libcurl4-openssl-dev libexpat1-dev libssl-dev zlib1g-dev \
9
+ && curl -fsSL -o /tmp/git.tar.xz \
10
+ "https://mirrors.edge.kernel.org/pub/software/scm/git/git-${GIT_VERSION}.tar.xz" \
11
+ && echo "${GIT_SHA256} /tmp/git.tar.xz" | sha256sum -c - \
12
+ && tar -xJf /tmp/git.tar.xz -C /tmp \
13
+ && cd "/tmp/git-${GIT_VERSION}" \
14
+ && make -j"$(nproc)" prefix=/usr/local sysconfdir=/etc NO_TCLTK=1 NO_PERL=1 NO_PYTHON=1 NO_GETTEXT=1 NO_RUST=1 \
15
+ DESTDIR=/out all install
16
+
1
17
  FROM node:22-slim
2
18
 
3
- RUN apt-get update && apt-get install -y \
4
- git curl gh ca-certificates \
19
+ COPY --from=git-build /out/usr/local /usr/local
20
+
21
+ # gh comes from GitHub's apt repo: Debian's lags by years and predates the
22
+ # hosts.yml / config.yml behaviour the CLI's read-only session mount relies on
23
+ RUN apt-get update \
24
+ && apt-get install -y --no-install-recommends curl ca-certificates less libcurl4 libexpat1 \
25
+ && install -m 0755 -d /etc/apt/keyrings \
26
+ && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
27
+ -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
28
+ && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
29
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
30
+ > /etc/apt/sources.list.d/github-cli.list \
31
+ && apt-get update \
32
+ && apt-get install -y --no-install-recommends gh \
5
33
  && rm -rf /var/lib/apt/lists/*
6
34
 
7
- # Curated client CLIs — keep in sync with src/clients.js
35
+ # System-level, not ~/.gitconfig: /home/agent is the persistent volume and
36
+ # shadows anything baked into home for existing installs
37
+ COPY gitconfig /etc/gitconfig
38
+ COPY sandbox-marker /etc/maesttro/sandbox
39
+
40
+ # Curated client CLIs — keep in sync with src/clients.ts
8
41
  RUN npm install -g \
9
42
  @anthropic-ai/claude-code \
10
43
  opencode-ai \
@@ -12,14 +45,18 @@ RUN npm install -g \
12
45
 
13
46
  RUN useradd -m -s /bin/bash agent
14
47
  USER agent
48
+ # Created as agent so a fresh volume inherits them; otherwise Docker creates
49
+ # ~/.config and ~/.claude as root when it sets up the nested read-only mounts
50
+ RUN mkdir -p /home/agent/.config/opencode /home/agent/.claude /home/agent/.codex
15
51
  WORKDIR /workspace
16
52
 
17
- # Bake curated default configs into each client's config path.
18
- # These become each client's out-of-the-box defaults inside the sandbox;
19
- # `maesttro update` refreshes them by rebuilding the image.
20
- COPY --chown=agent:agent configs/claude/ /home/agent/.claude/
21
- COPY --chown=agent:agent configs/opencode/ /home/agent/.config/opencode/
22
- COPY --chown=agent:agent configs/codex/ /home/agent/.codex/
53
+ # Client configs are applied at container start by `maesttro configure`
54
+ # (mounted from the host's install): /home/agent is a persistent volume, so
55
+ # anything baked here would only ever reach brand-new volumes.
23
56
 
24
- ENTRYPOINT ["/bin/bash", "-lc"]
57
+ # Starts as root only to remap agent to the host user's uid/gid (bind mounts
58
+ # keep host ownership on Linux), then drops to agent before running anything
59
+ USER root
60
+ COPY --chmod=755 entrypoint.sh /usr/local/bin/maesttro-entrypoint
61
+ ENTRYPOINT ["/usr/local/bin/maesttro-entrypoint"]
25
62
  CMD ["claude"]
@@ -0,0 +1,18 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ uid="${MAESTTRO_HOST_UID:-}"
5
+ gid="${MAESTTRO_HOST_GID:-$uid}"
6
+
7
+ if [ -n "$uid" ] && [ "$uid" != "$(id -u agent)" ]; then
8
+ groupmod --non-unique --gid "$gid" agent
9
+ usermod --non-unique --uid "$uid" --gid "$gid" agent
10
+ fi
11
+
12
+ owner="$(id -u agent):$(id -g agent)"
13
+ if [ -n "$uid" ] && [ "$(stat -c %u:%g /home/agent)" != "$owner" ]; then
14
+ find /home/agent -xdev -exec chown -h "$owner" {} + 2>/dev/null || true
15
+ fi
16
+
17
+ export HOME=/home/agent USER=agent LOGNAME=agent
18
+ exec setpriv --reuid=agent --regid=agent --init-groups /bin/bash -c "$*"
@@ -0,0 +1,8 @@
1
+ [url "https://github.com/"]
2
+ insteadOf = git@github.com:
3
+ insteadOf = ssh://git@github.com/
4
+ [credential "https://github.com"]
5
+ helper =
6
+ helper = !gh auth git-credential
7
+ [safe]
8
+ directory = *
@@ -0,0 +1 @@
1
+ maesttro sandbox
package/package.json CHANGED
@@ -1,15 +1,22 @@
1
1
  {
2
2
  "name": "maesttro",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Isolated Docker sandbox for running local coding agents (claude, opencode, codex, ...) under a dedicated bot GitHub identity, with curated configs baked in per client.",
5
+ "type": "module",
5
6
  "bin": {
6
- "maesttro": "bin/maesttro.js"
7
+ "maesttro": "dist/maesttro.js"
7
8
  },
8
9
  "files": [
9
- "bin",
10
- "src",
10
+ "dist",
11
11
  "docker"
12
12
  ],
13
+ "scripts": {
14
+ "build": "bun build src/cli.ts --target node --format esm --outfile dist/maesttro.js",
15
+ "prepack": "bun run build",
16
+ "test": "bun test ./tests",
17
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
18
+ "clean": "rm -rf dist node_modules"
19
+ },
13
20
  "repository": {
14
21
  "type": "git",
15
22
  "url": "git+https://github.com/esatto/maesttro.git",
package/bin/maesttro.js DELETED
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const { setup } = require("../src/setup")
4
- const { doctor } = require("../src/doctor")
5
- const { launch, list } = require("../src/launch")
6
- const { update } = require("../src/update")
7
-
8
- const [, , cmd, ...rest] = process.argv
9
-
10
- const HELP = `maesttro — isolated sandbox for local coding agents
11
-
12
- Usage:
13
- maesttro Launch the default client (claude) in the sandbox
14
- maesttro <client> Launch a specific client — see \`maesttro list\`
15
- maesttro list Show available clients
16
- maesttro setup [token] One-time: build image, create volume, authenticate bot identity
17
- maesttro update Update the package and rebuild the image with refreshed client configs
18
- maesttro doctor Health check — Docker, image, volume, bot auth, isolation, clients
19
- maesttro help Show this message
20
- `
21
-
22
- async function main() {
23
- switch (cmd) {
24
- case undefined:
25
- launch([])
26
- break
27
- case "setup":
28
- await setup(rest)
29
- break
30
- case "update":
31
- update()
32
- break
33
- case "doctor":
34
- doctor()
35
- break
36
- case "list":
37
- list()
38
- break
39
- case "help":
40
- case "--help":
41
- case "-h":
42
- console.log(HELP)
43
- break
44
- default:
45
- launch([cmd, ...rest])
46
- }
47
- }
48
-
49
- main()
package/src/clients.js DELETED
@@ -1,26 +0,0 @@
1
- // Curated list of supported agent clients. Adding a new one here:
2
- // 1. add the entry below
3
- // 2. add `RUN npm install -g <npmPackage>` to docker/Dockerfile
4
- // 3. create docker/configs/<key>/ with any default config to bake in
5
- // then `maesttro update` (or `agent setup` during dev) picks it up.
6
-
7
- module.exports = {
8
- claude: {
9
- label: "Claude Code",
10
- npmPackage: "@anthropic-ai/claude-code",
11
- binary: "claude",
12
- configDir: "/home/agent/.claude",
13
- },
14
- opencode: {
15
- label: "opencode",
16
- npmPackage: "opencode-ai",
17
- binary: "opencode",
18
- configDir: "/home/agent/.config/opencode",
19
- },
20
- codex: {
21
- label: "OpenAI Codex CLI",
22
- npmPackage: "@openai/codex",
23
- binary: "codex",
24
- configDir: "/home/agent/.codex",
25
- },
26
- }
package/src/doctor.js DELETED
@@ -1,78 +0,0 @@
1
- const { execSync } = require("child_process")
2
- const { IMAGE, VOLUME } = require("./paths")
3
- const clients = require("./clients")
4
-
5
- function check(label, fn) {
6
- try {
7
- const detail = fn()
8
- console.log(` [ok] ${label}${detail ? " — " + detail : ""}`)
9
- return true
10
- } catch (e) {
11
- console.log(` [fail] ${label}`)
12
- return false
13
- }
14
- }
15
-
16
- function doctor() {
17
- console.log("== maesttro doctor ==\n")
18
- let allOk = true
19
-
20
- allOk &= check("Docker daemon reachable", () => {
21
- execSync("docker info", { stdio: "ignore" })
22
- })
23
-
24
- allOk &= check("Sandbox image built", () => {
25
- const out = execSync(
26
- `docker image inspect ${IMAGE} --format "{{.Created}}"`,
27
- )
28
- .toString()
29
- .trim()
30
- return `built ${out}`
31
- })
32
-
33
- allOk &= check("Persistent volume exists", () => {
34
- execSync(`docker volume inspect ${VOLUME}`, { stdio: "ignore" })
35
- })
36
-
37
- allOk &= check("Bot identity authenticated", () => {
38
- const out = execSync(
39
- `docker run --rm -v ${VOLUME}:/home/agent ${IMAGE} "gh auth status 2>&1"`,
40
- ).toString()
41
- const match = out.match(/Logged in to github\.com account (\S+)/)
42
- return match ? match[1] : "authenticated"
43
- })
44
-
45
- allOk &= check(
46
- "Personal SSH key NOT visible inside sandbox (isolation intact)",
47
- () => {
48
- const out = execSync(
49
- `docker run --rm -v ${VOLUME}:/home/agent ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || echo MISSING"`,
50
- ).toString()
51
- if (!out.includes("MISSING") && !out.includes("No such file")) {
52
- throw new Error(
53
- "SSH key is reachable inside the sandbox — isolation broken",
54
- )
55
- }
56
- },
57
- )
58
-
59
- for (const [key, c] of Object.entries(clients)) {
60
- allOk &= check(`Client available: ${c.label}`, () => {
61
- execSync(`docker run --rm ${IMAGE} "which ${c.binary}"`, {
62
- stdio: "ignore",
63
- })
64
- })
65
- }
66
-
67
- console.log()
68
- if (allOk) {
69
- console.log("All checks passed.")
70
- } else {
71
- console.log(
72
- "Some checks failed — run `maesttro setup` or `maesttro update` to fix, or check the failing item above.",
73
- )
74
- process.exit(1)
75
- }
76
- }
77
-
78
- module.exports = { doctor }
package/src/launch.js DELETED
@@ -1,68 +0,0 @@
1
- const { spawnSync, execSync } = require("child_process")
2
- const fs = require("fs")
3
- const path = require("path")
4
- const os = require("os")
5
- const { IMAGE, VOLUME } = require("./paths")
6
- const clients = require("./clients")
7
-
8
- const DEFAULT_CLIENT = "claude"
9
-
10
- function ensureReady() {
11
- try {
12
- execSync(`docker image inspect ${IMAGE}`, { stdio: "ignore" })
13
- execSync(`docker volume inspect ${VOLUME}`, { stdio: "ignore" })
14
- } catch {
15
- console.error("Sandbox isn't set up yet. Run `maesttro setup` first.")
16
- process.exit(1)
17
- }
18
- }
19
-
20
- function list() {
21
- console.log("Available clients:\n")
22
- for (const [key, c] of Object.entries(clients)) {
23
- console.log(` ${key.padEnd(10)} ${c.label} (${c.npmPackage})`)
24
- }
25
- console.log(
26
- `\nDefault: ${DEFAULT_CLIENT}. Run \`maesttro <client>\` to pick one, or \`maesttro <client> -- <args>\` to pass args through.`,
27
- )
28
- }
29
-
30
- function launch(cmdArgs) {
31
- ensureReady()
32
-
33
- const cwd = process.cwd()
34
- const sharedSkills =
35
- process.env.AGENT_SHARED_SKILLS ||
36
- path.join(os.homedir(), "agent-shared", "skills")
37
-
38
- let clientKey = DEFAULT_CLIENT
39
- let extraArgs = []
40
-
41
- if (cmdArgs.length > 0 && clients[cmdArgs[0]]) {
42
- clientKey = cmdArgs[0]
43
- extraArgs = cmdArgs.slice(1)
44
- } else if (cmdArgs.length > 0) {
45
- // not a known client — treat the whole thing as a raw command, e.g. `maesttro bash`
46
- return runRaw(cmdArgs.join(" "), cwd, sharedSkills)
47
- }
48
-
49
- const client = clients[clientKey]
50
- const command = [client.binary, ...extraArgs].join(" ")
51
- runRaw(command, cwd, sharedSkills)
52
- }
53
-
54
- function runRaw(command, cwd, sharedSkills) {
55
- const mounts = ["-v", `${VOLUME}:/home/agent`, "-v", `${cwd}:/workspace`]
56
- if (fs.existsSync(sharedSkills)) {
57
- mounts.push("-v", `${sharedSkills}:/home/agent/.claude/skills:ro`)
58
- }
59
-
60
- const result = spawnSync(
61
- "docker",
62
- ["run", "-it", "--rm", ...mounts, "-w", "/workspace", IMAGE, command],
63
- { stdio: "inherit" },
64
- )
65
- process.exit(result.status ?? 0)
66
- }
67
-
68
- module.exports = { launch, list }
package/src/paths.js DELETED
@@ -1,12 +0,0 @@
1
- const path = require("path")
2
-
3
- // __dirname here is <install>/src — package root is one level up.
4
- // This works identically whether installed via `npm link`, `npm i -g .`,
5
- // or from the registry — npm always keeps the package's own files together.
6
- const PKG_ROOT = path.resolve(__dirname, "..")
7
- const DOCKERFILE_DIR = path.join(PKG_ROOT, "docker")
8
-
9
- const IMAGE = "maesttro-runner:latest"
10
- const VOLUME = "maesttro-home"
11
-
12
- module.exports = { PKG_ROOT, DOCKERFILE_DIR, IMAGE, VOLUME }
package/src/setup.js DELETED
@@ -1,101 +0,0 @@
1
- const { execSync, spawnSync } = require("child_process")
2
- const readline = require("readline")
3
- const fs = require("fs")
4
- const { DOCKERFILE_DIR, IMAGE, VOLUME } = require("./paths")
5
-
6
- function run(cmd, opts = {}) {
7
- return execSync(cmd, { stdio: "inherit", ...opts })
8
- }
9
-
10
- function ask(question) {
11
- const rl = readline.createInterface({
12
- input: process.stdin,
13
- output: process.stdout,
14
- })
15
- return new Promise((resolve) =>
16
- rl.question(question, (answer) => {
17
- rl.close()
18
- resolve(answer.trim())
19
- }),
20
- )
21
- }
22
-
23
- async function setup(args) {
24
- console.log("== maesttro setup ==\n")
25
-
26
- // 1. Docker present?
27
- try {
28
- execSync("docker info", { stdio: "ignore" })
29
- } catch {
30
- console.error(
31
- "Docker doesn't seem to be running. Start Docker Desktop (or the docker daemon) and try again.",
32
- )
33
- process.exit(1)
34
- }
35
-
36
- // 2. Build image from the Dockerfile bundled inside this package
37
- console.log(
38
- "-> Building sandbox image (this pulls node:22-slim + installs CLIs, first run only)...",
39
- )
40
- run(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`)
41
-
42
- // 3. Create persistent volume
43
- console.log("\n-> Ensuring persistent volume exists...")
44
- execSync(`docker volume create ${VOLUME}`, { stdio: "ignore" })
45
-
46
- // 4. Get bot token — file path arg, or interactive paste
47
- const tokenPath = args[0]
48
- let token
49
- if (tokenPath && fs.existsSync(tokenPath)) {
50
- token = fs.readFileSync(tokenPath, "utf8").trim()
51
- } else {
52
- console.log(
53
- "\nPaste the bot account's fine-grained GitHub token (input hidden not supported in plain readline — clear your scrollback after):",
54
- )
55
- token = await ask("Token: ")
56
- }
57
- if (!token) {
58
- console.error(
59
- "No token provided. Run `maesttro setup /path/to/token.txt` or paste one when prompted.",
60
- )
61
- process.exit(1)
62
- }
63
-
64
- // 5. Authenticate the bot identity into the volume
65
- console.log("\n-> Authenticating bot identity into the sandbox volume...")
66
- const auth = spawnSync(
67
- "docker",
68
- [
69
- "run",
70
- "-i",
71
- "--rm",
72
- "-v",
73
- `${VOLUME}:/home/agent`,
74
- IMAGE,
75
- "gh auth login --with-token && gh auth setup-git",
76
- ],
77
- { input: token, stdio: ["pipe", "inherit", "inherit"] },
78
- )
79
- if (auth.status !== 0) {
80
- console.error(
81
- "\nToken authentication failed — check the token is valid and has repo access.",
82
- )
83
- process.exit(1)
84
- }
85
-
86
- // 6. Verify isolation
87
- console.log("\n-> Verifying isolation...")
88
- console.log(" gh identity inside sandbox:")
89
- run(`docker run --rm -v ${VOLUME}:/home/agent ${IMAGE} "gh auth status"`)
90
- console.log(" personal SSH key should NOT be visible:")
91
- run(
92
- `docker run --rm -v ${VOLUME}:/home/agent ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || true"`,
93
- )
94
-
95
- console.log(
96
- "\nSetup complete. Run `maesttro` from any project directory to start a session,",
97
- )
98
- console.log("or `maesttro list` to see available clients.")
99
- }
100
-
101
- module.exports = { setup }
package/src/update.js DELETED
@@ -1,22 +0,0 @@
1
- const { execSync } = require("child_process")
2
- const { DOCKERFILE_DIR, IMAGE, PKG_ROOT } = require("./paths")
3
- const { name: packageName } = require(`${PKG_ROOT}/package.json`)
4
-
5
- function update() {
6
- console.log("== maesttro update ==\n")
7
-
8
- console.log(`-> Updating ${packageName} package...`)
9
- execSync(`npm i -g ${packageName}@latest`, { stdio: "inherit" })
10
-
11
- console.log("\n-> Rebuilding sandbox image with refreshed client configs...")
12
- execSync(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`, { stdio: "inherit" })
13
-
14
- console.log(
15
- "\nUpdate complete. Bot identity and your project files are untouched —",
16
- )
17
- console.log(
18
- "only the image (client versions + curated configs) was refreshed.",
19
- )
20
- }
21
-
22
- module.exports = { update }