mulmoclaude 0.5.2 → 0.5.3
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/Dockerfile.sandbox +100 -0
- package/bin/mulmoclaude.js +1 -1
- package/bin/prepare-dist.js +18 -2
- package/package.json +4 -2
- package/sandbox-entrypoint.sh +106 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
FROM node:22-slim
|
|
2
|
+
|
|
3
|
+
# Sandbox tool set (see #162). Grouped into layers by purpose so the
|
|
4
|
+
# rationale is obvious from the diff:
|
|
5
|
+
#
|
|
6
|
+
# 1. Core CLI — read local git history, fetch over HTTP, pipeline JSON,
|
|
7
|
+
# run Makefile-driven workspaces, handle archives, query SQLite.
|
|
8
|
+
# `ca-certificates` is needed because `node:22-slim` ships without
|
|
9
|
+
# a trust store. `ripgrep` backs Claude's grep tool.
|
|
10
|
+
# 2. Data analysis + plotting — Python with pandas/numpy/matplotlib
|
|
11
|
+
# and `requests` for API fetches. Graphviz for structural diagrams.
|
|
12
|
+
# ImageMagick for raster ops. Covers the "investment / data / chart"
|
|
13
|
+
# and "visualize this workflow" use cases in the issue.
|
|
14
|
+
# 3. Document + media conversion — pandoc bridges Markdown ↔ PDF/DOCX/
|
|
15
|
+
# HTML/EPUB for report output. ffmpeg covers audio/video transcode
|
|
16
|
+
# and probing. poppler-utils extracts text / images from PDFs.
|
|
17
|
+
# 4. Small extras — `tree` for directory listing, `bc` for arbitrary-
|
|
18
|
+
# precision arithmetic, `less` for pagers.
|
|
19
|
+
#
|
|
20
|
+
# Deferred on purpose:
|
|
21
|
+
#
|
|
22
|
+
# - `@mermaid-js/mermaid-cli` — pulls in headless Chromium (~300MB).
|
|
23
|
+
# Revisit as a separate layer if diagram generation becomes common.
|
|
24
|
+
# - `texlive-*` — ~150MB+ for marginal PDF quality gains over pandoc's
|
|
25
|
+
# default LaTeX-free path; add when a user actually needs math/
|
|
26
|
+
# journal-style typesetting.
|
|
27
|
+
# - `gh` CLI — now installed (Layer 1b). Auth via SANDBOX_MOUNT_CONFIGS=gh
|
|
28
|
+
# which mounts ~/.config/gh:ro into the container (#259).
|
|
29
|
+
#
|
|
30
|
+
# Runtime `pip install` is intentionally NOT wired up. Everything the
|
|
31
|
+
# agent might need for data work is preinstalled at build time so
|
|
32
|
+
# supply-chain risk stays out of the sandbox.
|
|
33
|
+
|
|
34
|
+
# Layer 1: core CLI + SSH client (needed for SSH agent forwarding #259)
|
|
35
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
36
|
+
git \
|
|
37
|
+
curl \
|
|
38
|
+
jq \
|
|
39
|
+
make \
|
|
40
|
+
ca-certificates \
|
|
41
|
+
openssh-client \
|
|
42
|
+
sqlite3 \
|
|
43
|
+
zip \
|
|
44
|
+
unzip \
|
|
45
|
+
ripgrep \
|
|
46
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
47
|
+
|
|
48
|
+
# Layer 1b: GitHub CLI (#164). Installed from the official apt repo.
|
|
49
|
+
# Auth is provided at runtime via SANDBOX_MOUNT_CONFIGS=gh which
|
|
50
|
+
# bind-mounts ~/.config/gh:ro (#259). On macOS where the token lives
|
|
51
|
+
# in the system keyring, the server falls back to GH_TOKEN env var.
|
|
52
|
+
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
53
|
+
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
54
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
55
|
+
> /etc/apt/sources.list.d/github-cli.list \
|
|
56
|
+
&& apt-get update && apt-get install -y --no-install-recommends gh \
|
|
57
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
58
|
+
|
|
59
|
+
# Layer 2: data analysis + plotting
|
|
60
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
61
|
+
python3 \
|
|
62
|
+
python3-pip \
|
|
63
|
+
graphviz \
|
|
64
|
+
imagemagick \
|
|
65
|
+
&& rm -rf /var/lib/apt/lists/* \
|
|
66
|
+
&& pip3 install --no-cache-dir --break-system-packages \
|
|
67
|
+
pandas \
|
|
68
|
+
numpy \
|
|
69
|
+
matplotlib \
|
|
70
|
+
requests
|
|
71
|
+
|
|
72
|
+
# Layer 3: document + media conversion
|
|
73
|
+
# LibreOffice core + Impress + Writer enable server-side PPTX→PDF and
|
|
74
|
+
# DOCX→PDF conversion for the attachment pipeline (#386). Only the
|
|
75
|
+
# headless renderer is used (`--headless --convert-to pdf`).
|
|
76
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
77
|
+
pandoc \
|
|
78
|
+
ffmpeg \
|
|
79
|
+
poppler-utils \
|
|
80
|
+
libreoffice-core \
|
|
81
|
+
libreoffice-impress \
|
|
82
|
+
libreoffice-writer \
|
|
83
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
84
|
+
|
|
85
|
+
# Layer 4: small extras
|
|
86
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
87
|
+
tree \
|
|
88
|
+
bc \
|
|
89
|
+
less \
|
|
90
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
91
|
+
|
|
92
|
+
RUN npm install -g @anthropic-ai/claude-code tsx
|
|
93
|
+
|
|
94
|
+
COPY sandbox-entrypoint.sh /sandbox-entrypoint.sh
|
|
95
|
+
RUN chmod +x /sandbox-entrypoint.sh
|
|
96
|
+
|
|
97
|
+
# No `USER node` — the entrypoint starts as root to fix /etc/passwd
|
|
98
|
+
# and socket permissions, then drops to HOST_UID:HOST_GID via setpriv.
|
|
99
|
+
WORKDIR /home/node/mulmoclaude
|
|
100
|
+
ENTRYPOINT ["/sandbox-entrypoint.sh"]
|
package/bin/mulmoclaude.js
CHANGED
package/bin/prepare-dist.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// ── Publish ───────────────────────────────────────────────────
|
|
32
32
|
// cd packages/mulmoclaude && npm publish --access public
|
|
33
33
|
|
|
34
|
-
import { cpSync, existsSync, rmSync } from "fs";
|
|
34
|
+
import { copyFileSync, cpSync, existsSync, rmSync } from "fs";
|
|
35
35
|
import { join, dirname } from "path";
|
|
36
36
|
import { fileURLToPath } from "url";
|
|
37
37
|
|
|
@@ -42,11 +42,16 @@ const rootDir = join(pkgDir, "..", "..");
|
|
|
42
42
|
// ── Clean ───────────────────────────────────────────────────
|
|
43
43
|
|
|
44
44
|
// Include `dist` so leftovers from the older pre-built-JS layout are
|
|
45
|
-
// wiped on re-run.
|
|
45
|
+
// wiped on re-run. Stale sandbox files are removed too so a renamed
|
|
46
|
+
// or deleted source file doesn't ride along into the tarball.
|
|
46
47
|
for (const dir of ["dist", "client", "server", "src"]) {
|
|
47
48
|
const target = join(pkgDir, dir);
|
|
48
49
|
if (existsSync(target)) rmSync(target, { recursive: true });
|
|
49
50
|
}
|
|
51
|
+
for (const file of ["Dockerfile.sandbox", "sandbox-entrypoint.sh"]) {
|
|
52
|
+
const target = join(pkgDir, file);
|
|
53
|
+
if (existsSync(target)) rmSync(target);
|
|
54
|
+
}
|
|
50
55
|
|
|
51
56
|
// ── Client dist (Vite build output) ─────────────────────────
|
|
52
57
|
// Copied to `client/` (not `dist/client/`) so the server's
|
|
@@ -89,5 +94,16 @@ cpSync(join(rootDir, "src"), join(pkgDir, "src"), {
|
|
|
89
94
|
});
|
|
90
95
|
console.log("✓ shared src/");
|
|
91
96
|
|
|
97
|
+
// ── Sandbox build context ───────────────────────────────────
|
|
98
|
+
// `server/system/docker.ts` builds the sandbox image via `docker build
|
|
99
|
+
// -f Dockerfile.sandbox .` with cwd = pkgDir. The Dockerfile in turn
|
|
100
|
+
// `COPY`s `sandbox-entrypoint.sh`, so both files must sit at pkgDir
|
|
101
|
+
// or sandbox mode silently falls back to unrestricted execution.
|
|
102
|
+
|
|
103
|
+
for (const file of ["Dockerfile.sandbox", "sandbox-entrypoint.sh"]) {
|
|
104
|
+
copyFileSync(join(rootDir, file), join(pkgDir, file));
|
|
105
|
+
}
|
|
106
|
+
console.log("✓ sandbox build context");
|
|
107
|
+
|
|
92
108
|
console.log("\nReady to publish. Run:");
|
|
93
109
|
console.log(" cd packages/mulmoclaude && npm publish --access public");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mulmoclaude",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "MulmoClaude — GUI-chat with Claude Code + long-term memory. One command to start.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"bin/",
|
|
16
16
|
"client/",
|
|
17
17
|
"server/",
|
|
18
|
-
"src/"
|
|
18
|
+
"src/",
|
|
19
|
+
"Dockerfile.sandbox",
|
|
20
|
+
"sandbox-entrypoint.sh"
|
|
19
21
|
],
|
|
20
22
|
"dependencies": {
|
|
21
23
|
"@google/genai": "^1.50.1",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Docker sandbox entrypoint for MulmoClaude.
|
|
3
|
+
#
|
|
4
|
+
# Runs as root to perform setup steps that require elevated privileges,
|
|
5
|
+
# then drops to the host user's UID:GID via setpriv (part of util-linux,
|
|
6
|
+
# already in node:22-slim — no extra install).
|
|
7
|
+
#
|
|
8
|
+
# Why not just `--user ${UID}:${GID}`?
|
|
9
|
+
# That flag runs the ENTIRE container (including this entrypoint)
|
|
10
|
+
# as the non-root user, which prevents writing to /etc/passwd and
|
|
11
|
+
# fixing socket permissions. The entrypoint-then-drop pattern is
|
|
12
|
+
# the standard Docker solution for "I need root setup but non-root
|
|
13
|
+
# runtime". See #259 for the full motivation.
|
|
14
|
+
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
# When HOST_UID is unset, the container was started with `--user`
|
|
18
|
+
# (non-SSH mode). Skip all root setup and exec directly — we're
|
|
19
|
+
# already running as the target user with zero capabilities.
|
|
20
|
+
if [ -z "${HOST_UID:-}" ]; then
|
|
21
|
+
exec "$@"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
TARGET_UID="$HOST_UID"
|
|
25
|
+
TARGET_GID="${HOST_GID:-1000}"
|
|
26
|
+
|
|
27
|
+
# 1. Add a /etc/passwd entry for the target UID if it doesn't exist.
|
|
28
|
+
# SSH refuses to operate when the running user has no passwd entry
|
|
29
|
+
# ("No user exists for uid NNN"). On macOS the host UID is
|
|
30
|
+
# typically 501, which isn't in the container's passwd (only root=0
|
|
31
|
+
# and node=1000 are).
|
|
32
|
+
if ! getent passwd "$TARGET_UID" > /dev/null 2>&1; then
|
|
33
|
+
echo "sandbox:x:${TARGET_UID}:${TARGET_GID}::/home/node:/bin/sh" >> /etc/passwd
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# 1b. Ensure /home/node is writable by the target user.
|
|
37
|
+
# The base image (node:22-slim) creates this directory owned by
|
|
38
|
+
# node:node (1000:1000). When running as a different UID (e.g. 501
|
|
39
|
+
# on macOS), Claude CLI and git/ssh can't create config files
|
|
40
|
+
# there (.ssh/, .gitconfig, etc.) without this chown.
|
|
41
|
+
chown -R "$TARGET_UID:$TARGET_GID" /home/node 2>/dev/null || true
|
|
42
|
+
|
|
43
|
+
# 2. Make the SSH agent socket accessible to the target user.
|
|
44
|
+
# Docker Desktop for Mac's magic socket (/run/host-services/
|
|
45
|
+
# ssh-auth.sock) is created as root:root mode 660. The target UID
|
|
46
|
+
# (e.g. 501) has no group membership to read it. A broad chmod is
|
|
47
|
+
# acceptable here because we're inside an isolated container with
|
|
48
|
+
# --cap-drop ALL — there's no other user to protect against.
|
|
49
|
+
if [ -S "${SSH_AUTH_SOCK:-}" ]; then
|
|
50
|
+
chmod 666 "$SSH_AUTH_SOCK" 2>/dev/null || true
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# 3. Restrict SSH agent usage to whitelisted hosts only.
|
|
54
|
+
# Without this, the container could use the forwarded agent to
|
|
55
|
+
# authenticate to ANY SSH server the host has access to (production
|
|
56
|
+
# servers, private infrastructure, etc.). The SSH config sets
|
|
57
|
+
# `IdentityAgent none` as the default and only enables the agent
|
|
58
|
+
# for explicitly allowed hosts.
|
|
59
|
+
#
|
|
60
|
+
# SANDBOX_SSH_ALLOWED_HOSTS: comma-separated list of hostnames.
|
|
61
|
+
# Default: github.com (the most common use case for git+ssh).
|
|
62
|
+
# Example: github.com,gitlab.com,bitbucket.org
|
|
63
|
+
if [ -S "${SSH_AUTH_SOCK:-}" ]; then
|
|
64
|
+
ALLOWED_HOSTS="${SANDBOX_SSH_ALLOWED_HOSTS:-github.com}"
|
|
65
|
+
SSH_DIR="/home/node/.ssh"
|
|
66
|
+
SSH_CONFIG="${SSH_DIR}/config"
|
|
67
|
+
|
|
68
|
+
mkdir -p "$SSH_DIR"
|
|
69
|
+
# SSH uses first-match-wins, so whitelisted hosts MUST come BEFORE
|
|
70
|
+
# the catch-all `Host *` block. Otherwise `IdentityAgent none`
|
|
71
|
+
# matches first and the per-host overrides are never reached.
|
|
72
|
+
cat > "$SSH_CONFIG" <<SSHEOF
|
|
73
|
+
# Auto-generated by sandbox-entrypoint.sh — do not edit.
|
|
74
|
+
# Restrict SSH agent forwarding to whitelisted hosts only.
|
|
75
|
+
# Override via SANDBOX_SSH_ALLOWED_HOSTS env var.
|
|
76
|
+
|
|
77
|
+
SSHEOF
|
|
78
|
+
|
|
79
|
+
# Per-host overrides FIRST (most-specific → least-specific)
|
|
80
|
+
echo "$ALLOWED_HOSTS" | tr ',' '\n' | while read -r host; do
|
|
81
|
+
host=$(echo "$host" | tr -d ' ')
|
|
82
|
+
if [ -n "$host" ]; then
|
|
83
|
+
printf "Host %s\n IdentityAgent %s\n StrictHostKeyChecking accept-new\n\n" "$host" "$SSH_AUTH_SOCK" >> "$SSH_CONFIG"
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
# Catch-all LAST: block agent for everything else
|
|
88
|
+
cat >> "$SSH_CONFIG" <<SSHEOF
|
|
89
|
+
Host *
|
|
90
|
+
IdentityAgent none
|
|
91
|
+
StrictHostKeyChecking accept-new
|
|
92
|
+
SSHEOF
|
|
93
|
+
|
|
94
|
+
chmod 700 "$SSH_DIR"
|
|
95
|
+
chmod 600 "$SSH_CONFIG"
|
|
96
|
+
chown -R "${TARGET_UID}:${TARGET_GID}" "$SSH_DIR"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# 4. Drop privileges and exec the actual command (typically `claude`).
|
|
100
|
+
# setpriv is part of util-linux, already present in node:22-slim.
|
|
101
|
+
# --init-groups initialises supplementary groups from /etc/group.
|
|
102
|
+
# --inh-caps=-all clears inheritable capabilities so the child
|
|
103
|
+
# process (claude) runs with zero capabilities even though the
|
|
104
|
+
# container was started with CHOWN/FOWNER/DAC_OVERRIDE/SETUID/SETGID
|
|
105
|
+
# for the entrypoint's setup steps.
|
|
106
|
+
exec setpriv --reuid="$TARGET_UID" --regid="$TARGET_GID" --init-groups --inh-caps=-all -- "$@"
|