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.
@@ -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"]
@@ -104,7 +104,7 @@ Options:
104
104
  }
105
105
 
106
106
  if (args.includes("--version")) {
107
- console.log("mulmoclaude 0.5.1");
107
+ console.log("mulmoclaude 0.5.3");
108
108
  process.exit(0);
109
109
  }
110
110
 
@@ -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.2",
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 -- "$@"