Grok Bot、dots、MuseのVM構成を比較する

Grok Bot、dots、MuseのVM構成を比較する

パーソナルAIエージェントの動作環境を調べてみました。Grok Bot、dots、Muse の 3 つのサービスについて、CPU やメモリ、インストール済みツールなどを比較調査しており、その結果と調査方法をまとめています。
2026.11.02

はじめに

dots、Muse、Grok Botには、エージェントが作業に使うクラウド上のコンピューターがあります。ブラウザーを操作したり、ファイルを作成したり、コマンドを実行したりするための環境です。

使っているうちに、作業に使うVMのCPUやメモリ、OSの構成も気になりました。

今回は、3つのVMに同じシェルスクリプトを配置し、CPUやメモリ、ストレージの構成と、インストール済みのツールを調べてみます。

記事では、調査に使ったスクリプトと、各サービスのVMに入って実行するまでの手順も紹介します。3サービスの比較としてだけでなく、エージェントの作業環境を自分で調べるときの手順としても使えるようにしました。

結論

今回確認した3つのVMの特徴は次のとおりです。

  • Grok Botはメモリ総量が約16GiBと最も大きいが、調査時に利用可能だったのは約3.2GiB
  • dotsは論理CPUが9個と最も多く、利用可能メモリも約8.6GiBと最も多い
  • Museは4 CPU/約8GiBと最も小さく、利用可能メモリは約0.5GiB

調査は2026年11月2日の8:36〜9:30(JST)に、各環境で同じスクリプトを実行して行いました。Grok Botとdotsは画面上のターミナル、MuseはSSH接続したシェルから実行しています。実行手順は各サービスの節に、EC2との費用比較などの感想は記事の最後にまとめています。

利用プランと料金

今回利用したプランと、その月額料金は次のとおりです[1]。料金は2026年11月2日時点の月払いの表示です。

項目 Grok Bot dots Muse
今回の利用プラン The X SubscriptionのPremium ChatGPT Pro 100 無料枠
月額料金 $30(税別)[2] 16,800円(消費税込み)[3] 0円[4]

OSとCPU

項目 Grok Bot dots Muse
OS Debian GNU/Linux 13 (trixie) Debian GNU/Linux 13 (trixie) Ubuntu 24.04.5 LTS
カーネル Linux 6.12.94+ Linux 6.18.57 Linux 7.0.0-39-generic
アーキテクチャ x86_64 x86_64 x86_64
CPUモデルの表示 Intel(R) Xeon(R) Processor AMD EPYC 9V74 80-Core Processor AMD EPYC 9D25 126-Core Processor
論理CPU数 8 9 4

CPUはGrok BotがIntel、dotsとMuseがAMDでした[5]。Grok Botの表示はIntel(R) Xeon(R) Processorまでで、世代や型番は分かりません。

メモリ

項目 Grok Bot dots Muse
メモリ総量 約15.63GiB 約9.73GiB 約7.75GiB
利用可能メモリ(MemAvailable) 約3.19GiB 約8.56GiB 約0.46GiB
利用可能メモリの割合(総量比) 約20.4% 約87.9% 約6.0%
スワップ総量 0B 0B 0B
所属cgroupのメモリ使用量(memory.current) 約4.28GiB 所属先を対応付けられず、未確認 読み取れず、未確認

調査時点で新しく使えたメモリ(MemAvailable)は、dotsが約8.56GiBと最も多く、Grok Botは約3.19GiB、Museは約0.46GiBでした[6]。3環境ともスワップは設定されていませんでした。

Grok Botは、OS全体の使用量(MemTotalとMemAvailableの差)が約12.44GiBだったのに対し、作業環境のcgroupの使用量(memory.current)は約4.28GiBでした[7]。差の約8GiBは、作業環境のcgroupの外(他のプロセスやカーネルなど)で使われている分です。

ストレージ

項目 Grok Bot dots Muse
ホームのファイルシステムの容量と種別 約127.39GiB / ext2/ext3と表示 約62.94GiB / overlayfs 7.50GiB / overlayfs
ホームのファイルシステムの空き容量 約119.40GiB 約59.57GiB 約7.42GiB
ルートのファイルシステムの容量と種別 ホームと同じファイルシステム 約4.87GiB / tmpfs ホームと同じファイルシステム
ルートのファイルシステムの空き容量 ホームと同じファイルシステム 約4.87GiB ホームと同じファイルシステム

ホームの容量はGrok Botが約127GiBと最も大きく、Museは7.50GiBでした。dotsだけはルートが約4.87GiBのtmpfsで、ホームとは別のファイルシステムです。3環境とも、スクリプトの実行ディレクトリはホームと同じファイルシステムでした。

仮想化とリソース制限

項目 Grok Bot dots Muse
cgroupによるCPUとメモリの上限 現グループはcpu.max=max 100000、memory.max=max。親階層は未確認 所属先とマウントを対応付けられず、未確認 所属先は対応付けできたが、cpu.maxとmemory.maxは読み取れず、未確認
仮想化の検出 lscpuはKVM、systemd-detect-virtはcontainer-other lscpuはKVM、systemd-detect-virtはcontainer-other lscpuはKVM、systemd-detect-virtはsystemd-nspawn
DMIのシステムベンダー 読み取れず 読み取れず Cloud Hypervisor
GPU 確認用コマンドが見つからず、未確認 確認用コマンドが見つからず、未確認 確認用コマンドが見つからず、未確認

3環境ともlscpuはKVMを示しましたが、systemd-detect-virtの結果はGrok Botとdotsがcontainer-other、Museがsystemd-nspawnと分かれました。DMIのベンダーを読み取れたのはMuseだけです。cgroupの上限は、Grok Botの現グループがmaxだった以外は読み取れませんでした。

インストール済みのツール

項目 Grok Bot dots Muse
Python(python3) 3.13.5 3.13.5 3.12.3
Node.js / npm 24.22.1 / 11.21.1 nodeとnpmが見つからず 24.22.1 / npmが見つからず
Git 2.47.3 2.52.0 2.43.0
FFmpeg 7.1.5-0+deb13u1 7.1.5-0+deb13u1 8.1.2
ブラウザー Google Chrome 156.0.8078.47 Chromium 155.0.8059.79 調査対象のコマンドは見つからず
調査時の実行ユーザー UID 1000 UID 1000 UID 0(root)

dotsではnodeとnpm、Museではブラウザーのコマンドが見つからず[8]、Museだけはrootで実行されていました。

スクリプトの説明

各VMで同じ項目を確認できるよう、調査用のsurvey.shを用意しました。Linux上のBashで動作し、追加のパッケージをインストールせずに実行できます。

スクリプトの全体は以下です。

調査スクリプト
survey.sh
#!/usr/bin/env bash
# A standalone, offline Linux survey. Bash 3.2+; no package installation.
# Sourceable so the cgroup reader can be tested against fixture files.
# Single-quoted snippets are intentionally evaluated by awk or a child Bash.
# shellcheck disable=SC2016

SURVEY_VERSION=1.0.0

usage() {
    cat <<'EOF'
Usage: bash survey.sh LABEL [--plan TEXT] [--note TEXT] [--output DIR] [--workdir DIR]

  LABEL         grok-bot / muse / dots, or another short ASCII label
  --plan        Manually recorded subscription plan (default: unknown)
  --note        How this terminal was opened; do not include secrets
  --output      Parent directory for a new result folder (default: ./results)
  --workdir     Filesystem to inspect as the workspace (default: current directory)

Runs on Linux only. Reads system information and writes a NEW result folder.
No network requests, sudo, installs, benchmarks, or persistence experiments.
EOF
}

cell() {
    # One TSV cell; Markdown escaping happens only when rendering Markdown.
    printf '%s' "$1" | tr '\t\r\n' '   '
}

fact() {
    printf '%s\t%s\t%s\t%s\n' "$1" "$2" "$(cell "$3")" "$(cell "$4")" >> "$FACTS"
}

probe() {
    local key=$1 description=$2 rc=0
    shift 2
    LAST_OUTPUT='' LAST_STATUS=unavailable
    if ! command -v "$1" >/dev/null 2>&1; then
        LAST_OUTPUT="command not found: $1"
        rc=127
    else
        if command -v timeout >/dev/null 2>&1; then
            LAST_OUTPUT=$(timeout -k 2s 10s "$@" 2>&1) || rc=$?
        else
            LAST_OUTPUT=$("$@" 2>&1) || rc=$?
        fi
        case $rc in
            0) LAST_STATUS=observed ;;
            124|137) LAST_STATUS=timeout ;;
            *) LAST_STATUS=error ;;
        esac
    fi
    # Logs contain allowlisted commands, not an environment/process/mount dump.
    printf '\n[%s]\n$ %s\nexit_code=%s status=%s\n%s\n' \
        "$key" "$description" "$rc" "$LAST_STATUS" "$LAST_OUTPUT" >> "$RAW"
}

measure() {
    local key=$1 description=$2
    probe "$@"
    if [[ $LAST_STATUS == observed && -n $LAST_OUTPUT ]]; then
        fact "$key" observed "$LAST_OUTPUT" "$description"
    elif [[ $LAST_STATUS == observed ]]; then
        fact "$key" unknown unknown "$description: empty output"
    else
        fact "$key" "$LAST_STATUS" unknown "$description; see commands.txt"
    fi
}

read_cgroup_value() {
    local key=$1 path=$2 description=$3
    if [[ -r $path ]]; then
        measure "$key" "$description" cat "$path"
    else
        LAST_OUTPUT='' LAST_STATUS=unavailable
        fact "$key" unavailable unknown "$description: absent or unreadable"
    fi
}

update_cpu_min() {
    local quota=$1 period=$2 candidate
    if [[ $quota =~ ^[0-9]+$ && $period =~ ^[0-9]+$ ]] && (( period > 0 )); then
        candidate=$(awk -v q="$quota" -v p="$period" 'BEGIN {printf "%.6f", q/p}')
        CPU_MIN=$(awk -v old="$CPU_MIN" -v new="$candidate" \
            'BEGIN {if (old == "" || new+0 < old+0) print new; else print old}')
    fi
}

update_memory_min() {
    local value=$1
    if [[ $value =~ ^[0-9]+$ ]]; then
        MEMORY_MIN=$(awk -v old="$MEMORY_MIN" -v new="$value" \
            'BEGIN {if (old == "" || new+0 < old+0) printf "%.0f", new; else print old}')
    fi
}

walk_cgroup() {
    local kind=$1 mount=$3 level=0 directory=$2 file quota period memory prefix
    local files=()
    case $kind in
        v2) files=(cpu.max cpu.max.burst cpu.stat cpuset.cpus.effective memory.max memory.high memory.current memory.swap.max pids.max) ;;
        cpu) files=(cpu.cfs_quota_us cpu.cfs_period_us cpu.stat) ;;
        memory) files=(memory.limit_in_bytes memory.usage_in_bytes memory.use_hierarchy memory.memsw.limit_in_bytes) ;;
        cpuset) files=(cpuset.cpus cpuset.effective_cpus) ;;
        pids) files=(pids.max) ;;
    esac
    while :; do
        prefix="cgroup.$kind.level$level"
        quota='' period='' memory=''
        for file in "${files[@]}"; do
            read_cgroup_value "$prefix.$file" "$directory/$file" "cat <cgroup:$kind:level$level>/$file"
            [[ $LAST_STATUS == observed ]] || continue
            case $file in
                cpu.max) read -r quota period <<< "$LAST_OUTPUT" ;;
                cpu.cfs_quota_us) quota=$LAST_OUTPUT ;;
                cpu.cfs_period_us) period=$LAST_OUTPUT ;;
                memory.max) memory=$LAST_OUTPUT ;;
            esac
        done
        update_cpu_min "$quota" "$period"
        # v1 memory parents may have use_hierarchy=0. Do not treat every parent
        # limit as effective; keep the original per-level values for inspection.
        [[ $kind != v2 ]] || update_memory_min "$memory"
        [[ $directory != "$mount" ]] || break
        directory=${directory%/*}
        [[ -n $directory ]] || directory=/
        level=$((level + 1))
        if (( level >= 128 )); then
            fact "cgroup.$kind.truncated" unknown yes 'Stopped after 128 visible levels'
            break
        fi
    done
    fact "cgroup.$kind.levels_read" observed "$((level + 1))" 'Level 0 = current group; higher levels = visible ancestors'
}

inspect_cgroups() {
    local membership_file=$1 mountinfo_file=$2 hierarchy controllers group
    local fs root mount options relative candidate kind controller found=0
    local seen='|' mounts kinds=()
    CPU_MIN='' MEMORY_MIN=''
    if [[ ! -r $membership_file || ! -r $mountinfo_file ]]; then
        fact cgroup.discovery unavailable unknown 'proc cgroup/mountinfo unavailable'
        return
    fi
    # Do not publish these files: cgroup paths and mount sources can contain IDs.
    mounts=$(awk '
        { for (i=7; i<=NF; i++) if ($i=="-") {
            if ($(i+1)=="cgroup" || $(i+1)=="cgroup2")
                printf "%s\t%s\t%s\t%s\n", $(i+1), $4, $5, $(i+3)
            break
        }}' "$mountinfo_file")
    while IFS=: read -r hierarchy controllers group; do
        [[ $group == /* ]] || continue
        # Never traverse outside a mounted hierarchy for unexpected proc data.
        case "/${group#/}/" in */../*|*/./*) continue ;; esac
        kinds=()
        if [[ $hierarchy == 0 && -z $controllers ]]; then
            kinds=(v2)
        else
            for controller in cpu memory cpuset pids; do
                case ",$controllers," in *",$controller,"*) kinds+=("$controller") ;; esac
            done
        fi
        for kind in "${kinds[@]-}"; do
            [[ -n $kind ]] || continue
            case "$seen" in *"|$kind|"*) continue ;; esac
            while IFS=$'\t' read -r fs root mount options; do
                [[ -n $fs ]] || continue
                if [[ $kind == v2 ]]; then
                    [[ $fs == cgroup2 ]] || continue
                else
                    [[ $fs == cgroup ]] || continue
                    case ",$options," in *",$kind,"*) ;; *) continue ;; esac
                fi
                # Linux mountinfo encodes spaces, tabs, newlines and backslashes.
                printf -v root '%b' "$root"
                printf -v mount '%b' "$mount"
                if [[ $group == "$root" ]]; then relative=''
                elif [[ $root == / ]]; then relative=$group
                elif [[ $group == "$root/"* ]]; then relative=${group#"$root"}
                else continue
                fi
                candidate="${mount%/}$relative"
                [[ -d $candidate ]] || continue
                # Both are kernel-supplied paths, but canonicalize to enforce
                # the boundary even for fixture files or unusual bind mounts.
                mount=$(cd -- "$mount" && pwd -P) || continue
                candidate=$(cd -- "$candidate" && pwd -P) || continue
                [[ $candidate == "$mount" || $candidate == "$mount/"* || $mount == / ]] || continue
                fact "cgroup.$kind.mapping" observed mapped 'proc membership matched mountinfo root; paths omitted'
                walk_cgroup "$kind" "$candidate" "$mount"
                seen="$seen$kind|"
                found=$((found + 1))
                break
            done <<< "$mounts"
        done
    done < "$membership_file"
    fact cgroup.hierarchies_mapped observed "$found" 'proc membership and visible cgroup mounts'
    if [[ -n $CPU_MIN ]]; then
        fact cgroup.cpu_quota_visible_min_cores derived "$CPU_MIN" 'Minimum observed quota/period; shared group budget, not dedicated cores'
    else
        fact cgroup.cpu_quota_visible_min_cores unknown unknown 'No finite readable quota found; NOT evidence of unrestricted CPU'
    fi
    if [[ -n $MEMORY_MIN ]]; then
        fact cgroup.v2_memory_visible_min_bytes derived "$MEMORY_MIN" 'Minimum observed v2 memory.max; shared group limit'
    else
        fact cgroup.v2_memory_visible_min_bytes unknown unknown 'No finite readable v2 limit; inspect v1 rows if present'
    fi
    fact cgroup.visibility_limit note 'Visible hierarchy only' 'Hidden ancestors, hypervisor limits and other workloads are not measurable here'
}

inspect_storage() {
    local role=$1 path=$2 number prior same='' id
    if [[ ! -d $path ]]; then
        fact "disk.$role" unavailable unknown 'Directory absent or unreadable'
        return
    fi
    # df sources/mountpoints can disclose remote shares or usernames. Select only
    # numeric columns, and explicitly label the log as filtered command output.
    probe "disk.$role.capacity" "df -Pk <path:$role> | awk <numeric columns only>" \
        bash -c 'set -o pipefail; df -Pk -- "$1" | awk "NR==2 {print \$2, \$3, \$4, \$5}"' _ "$path"
    if [[ $LAST_STATUS == observed && $LAST_OUTPUT =~ ^[0-9]+\ [0-9]+\ [0-9]+\ [0-9]+%$ ]]; then
        read -r number _ _ _ <<< "$LAST_OUTPUT"
        fact "disk.$role.total_kib" observed "$number" "Filtered df -Pk; raw log disk.$role.capacity"
        number=$(awk '{print $3}' <<< "$LAST_OUTPUT")
        fact "disk.$role.available_kib" observed "$number" "Available to unprivileged users; raw log disk.$role.capacity"
    else
        fact "disk.$role.capacity" unknown unknown "See commands.txt: disk.$role.capacity"
    fi
    measure "disk.$role.fstype" "stat -f -c %T <path:$role>" stat -f -c %T -- "$path"
    # st_dev is kept internally to identify duplicate filesystems, not published.
    id=$(stat -c %d -- "$path" 2>/dev/null) || id=''
    if [[ -n $id ]]; then
        for prior in "${STORAGE_IDS[@]-}"; do
            [[ -n $prior ]] || continue
            [[ ${prior#*:} != "$id" ]] || same=${prior%%:*}
        done
        STORAGE_IDS+=("$role:$id")
        fact "disk.$role.same_filesystem_as" observed "${same:-none_of_the_preceding_paths}" 'Compared stat st_dev; do not sum duplicate capacities'
    else
        fact "disk.$role.same_filesystem_as" unknown unknown 'stat st_dev unavailable'
    fi
}

render_summary() {
    cat <<'EOF'
# Agent VM survey

この結果は「スクリプトを実行した環境」の観測です。サービス名・プラン・実行先の説明は入力者の申告であり、自動検証していません。

- observed: 観測値、derived: 観測値からの計算、declared: 入力者の申告。
- unavailable / error / timeout / unknown: 未確認。機能・制限が存在しないという意味ではありません。
- CPUモデル内のコア数やゲストのトポロジーから、専有する物理コア数は断定できません。
- cgroupの上限は同じグループ内で共有されます。見える親階層までの値で、保証性能ではありません。
- GPUのツールやデバイスが見つからなくても、ホストのGPU不在は断定できません。
- ストレージ容量はファイルシステム単位です。ユーザークォータやバックエンドの実容量は未確認です。
- 永続性・ネットワーク性能・推論基盤は今回の調査対象外です。
- 実行コマンド・終了コード・出力は commands.txt、比較用データは facts.tsv にあります。

| 項目 | 状態 | 値 | 根拠・注記 |
| --- | --- | --- | --- |
EOF
    awk -F '\t' 'NR>1 {for(i=1;i<=4;i++) {gsub(/&/, "\\&amp;", $i); gsub(/</, "\\&lt;", $i); gsub(/\|/, "\\&#124;", $i)} printf "| %s | %s | %s | %s |\n", $1,$2,$3,$4}' "$FACTS"
}

main() {
    set -u
    set -o pipefail
    export LC_ALL=C
    umask 077
    local label='' plan=unknown note=unknown output=./results workspace=$PWD option
    local started outdir tool field metric hash
    [[ $# -gt 0 ]] || { usage; return 2; }
    case $1 in --help|-h) usage; return 0 ;; esac
    label=$1; shift
    [[ $label =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$ ]] || { printf 'Invalid LABEL\n' >&2; return 2; }
    while [[ $# -gt 0 ]]; do
        option=$1
        [[ $# -ge 2 ]] || { printf 'Missing value: %s\n' "$option" >&2; return 2; }
        case $option in
            --plan) plan=$2 ;; --note) note=$2 ;; --output) output=$2 ;; --workdir) workspace=$2 ;;
            *) printf 'Unknown option: %s\n' "$option" >&2; return 2 ;;
        esac
        shift 2
    done
    [[ $(uname -s) == Linux ]] || { printf 'Linux only. Run this in the agent cloud terminal, not your Mac.\n' >&2; return 2; }
    for tool in awk bash cat cp date mkdir mktemp stat tr uname; do
        command -v "$tool" >/dev/null 2>&1 || { printf 'Required command missing: %s\n' "$tool" >&2; return 2; }
    done
    [[ -d $workspace ]] || { printf 'Workspace is not a directory\n' >&2; return 2; }
    started=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    mkdir -p -- "$output" || return 1
    outdir=$(mktemp -d "$output/$label-$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX") || return 1
    FACTS=$outdir/facts.tsv RAW=$outdir/commands.txt
    printf 'key\tstatus\tvalue\tsource\n' > "$FACTS"
    printf 'agent-vm-survey %s\nstarted_utc=%s\n' "$SURVEY_VERSION" "$started" > "$RAW"
    printf 'Collecting %s ...\n' "$label"
    fact survey.version observed "$SURVEY_VERSION" 'survey.sh'
    fact survey.started_utc observed "$started" 'date -u'
    fact survey.label declared "$label" 'CLI argument; service identity is not independently verified'
    fact survey.plan declared "$plan" 'CLI argument'
    fact survey.execution_note declared "$note" 'CLI argument'
    fact survey.timeout_available observed "$(command -v timeout >/dev/null 2>&1 && printf yes || printf no)" '10s per command when GNU-compatible timeout is available'
    probe os.release 'cat /etc/os-release (never sourced)' cat /etc/os-release
    if [[ $LAST_STATUS == observed ]]; then
        for field in ID VERSION_ID PRETTY_NAME; do
            metric=$(awk -v key="$field" 'index($0,key"=")==1 {sub(/^[^=]*=/, ""); gsub(/^"|"$/, ""); print; exit}' <<< "$LAST_OUTPUT")
            if [[ -n $metric ]]; then fact "os.$field" observed "$metric" '/etc/os-release'
            else fact "os.$field" unknown unknown 'Field absent from /etc/os-release'; fi
        done
    else
        fact os.release "$LAST_STATUS" unknown 'See commands.txt'
    fi
    measure os.kernel 'uname -sr' uname -sr
    measure os.architecture 'uname -m' uname -m
    measure process.uid 'id -u (numeric only)' id -u
    measure process.limits 'ulimit -a (in inherited subshell)' bash -c 'ulimit -a'
    probe cpu.lscpu 'lscpu' lscpu
    if [[ $LAST_STATUS == observed ]]; then
        metric=$(awk -F: '/^Model name:/ {sub(/^[ \t]+/, "", $2); print $2; exit}' <<< "$LAST_OUTPUT")
        if [[ -n $metric ]]; then fact cpu.model observed "$metric" 'lscpu; full topology in commands.txt'
        else fact cpu.model unknown unknown 'Model name absent; inspect lscpu in commands.txt'; fi
    else
        measure cpu.model 'CPU model fields in /proc/cpuinfo' awk -F: '/^(model name|Hardware|Processor)[ \t]*:/ {sub(/^[ \t]+/, "", $2); print $2; exit}' /proc/cpuinfo
    fi
    measure cpu.online 'getconf _NPROCESSORS_ONLN' getconf _NPROCESSORS_ONLN
    measure cpu.nproc 'env -u OMP_NUM_THREADS -u OMP_THREAD_LIMIT nproc' env -u OMP_NUM_THREADS -u OMP_THREAD_LIMIT nproc
    measure cpu.nproc_all 'nproc --all' nproc --all
    measure cpu.allowed_list 'Cpus_allowed_list from /proc/<survey-pid>/status' awk '/^Cpus_allowed_list:/ {print $2}' "/proc/$$/status"
    probe memory.meminfo 'Selected capacity fields in /proc/meminfo' awk '/^(MemTotal|MemAvailable|SwapTotal|SwapFree):/ {print}' /proc/meminfo
    if [[ $LAST_STATUS == observed ]]; then
        for field in MemTotal MemAvailable SwapTotal SwapFree; do
            metric=$(awk -v key="$field:" '$1==key {printf "%.0f", $2*1024}' <<< "$LAST_OUTPUT")
            if [[ -n $metric ]]; then fact "memory.$field.bytes" observed "$metric" '/proc/meminfo kB multiplied by 1024'
            else fact "memory.$field.bytes" unknown unknown 'Field absent from /proc/meminfo'; fi
        done
    else
        fact memory.meminfo "$LAST_STATUS" unknown 'See commands.txt'
    fi
    probe memory.free 'free -b' free -b
    inspect_cgroups "/proc/$$/cgroup" "/proc/$$/mountinfo"
    STORAGE_IDS=()
    inspect_storage root /
    inspect_storage home "${HOME:-/nonexistent}"
    inspect_storage work "$workspace"
    probe virtualization.detect 'systemd-detect-virt (exit 1 + none means none detected)' systemd-detect-virt
    if [[ $LAST_OUTPUT == none ]]; then
        fact virtualization.detect observed none_detected 'systemd-detect-virt; not proof of bare metal'
    elif [[ $LAST_STATUS == observed ]]; then
        fact virtualization.detect observed "$LAST_OUTPUT" 'systemd-detect-virt'
    else
        fact virtualization.detect "$LAST_STATUS" unknown 'See commands.txt'
    fi
    for field in sys_vendor product_name board_vendor; do
        read_cgroup_value "virtualization.dmi.$field" "/sys/class/dmi/id/$field" "cat /sys/class/dmi/id/$field"
    done
    probe gpu.pci 'lspci -nn (display-class devices only in facts.tsv)' lspci -nn
    if [[ $LAST_STATUS == observed ]]; then
        metric=$(awk '/VGA compatible controller|3D controller|Display controller/' <<< "$LAST_OUTPUT")
        fact gpu.pci_display_devices observed "${metric:-no_display_class_device_visible}" 'Filtered lspci -nn; host GPU presence remains unknown'
    else
        fact gpu.pci_display_devices "$LAST_STATUS" unknown 'lspci unavailable or failed'
    fi
    measure gpu.nvidia 'nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader' \
        nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
    for tool in python3 python node npm git ffmpeg chromium chromium-browser google-chrome firefox; do
        if [[ $tool == ffmpeg ]]; then
            probe "tool.$tool" "$tool -version" "$tool" -version
        else
            probe "tool.$tool" "$tool --version" "$tool" --version
        fi
        if [[ $LAST_STATUS == observed ]]; then
            metric=${LAST_OUTPUT%%$'\n'*}
            fact "tool.$tool" observed "$metric" 'Version command; full output in commands.txt'
        else
            fact "tool.$tool" "$LAST_STATUS" unknown 'See commands.txt'
        fi
    done
    fact survey.finished_utc observed "$(date -u +%Y-%m-%dT%H:%M:%SZ)" 'date -u'
    cp -- "${BASH_SOURCE[0]}" "$outdir/survey.sh" || return 1
    if command -v sha256sum >/dev/null 2>&1; then
        hash=$(sha256sum "$outdir/survey.sh")
        fact survey.script_sha256 observed "${hash%% *}" 'sha256sum survey.sh'
    fi
    render_summary > "$outdir/summary.md" || return 1
    if command -v sha256sum >/dev/null 2>&1; then
        (cd -- "$outdir" && sha256sum survey.sh facts.tsv commands.txt summary.md > SHA256SUMS) || return 1
    fi
    printf 'Done: %s\nRead summary.md; compare facts.tsv; evidence in commands.txt.\n' "$outdir"
}

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
    main "$@"
fi

取得する情報は次のとおりです。lscpuやdfなどのコマンドの出力と、/proc配下の情報を使います。

項目 確認する内容
OS ディストリビューション、カーネル、アーキテクチャ
CPU CPUモデル、OSが認識するCPU数、プロセスが利用できるCPUの範囲
メモリ メモリ総量、空き容量、Swap
リソース制限 cgroupに設定されたCPU時間、メモリ、プロセス数の上限
ストレージ ルート、ホーム、作業ディレクトリの容量、空き容量、ファイルシステム
仮想化とGPU 仮想化方式の手掛かり、VMから見えるGPUデバイス
標準ツール Python、Node.js、Git、FFmpeg、ブラウザーなどのバージョン

CPU数やメモリ総量に加えて、cgroupの設定も確認します。cgroupは、プロセスのグループごとにリソースを管理するLinuxの仕組みです。たとえば、lscpuで複数のCPUが見えていても、グループ全体で使えるCPU時間に別の上限が設定されている場合があります。

このため、OSから見える値とcgroupの上限を分けて記録します。VM内から読み取れる範囲を調べるので、取得できない項目は未確認として扱います。cgroupの設定については、Linux Kernelのドキュメントを参考にしました。

調査結果は、一覧で読めるsummary.md、比較用のfacts.tsv、コマンドの出力を残すcommands.txtに保存します。記事に数値を載せるときに、元の出力も確認できるようにしました。

利用できるオプションは、bash survey.sh --helpで確認できます。

環境で実行

各VMにスクリプトを配置して、調査結果を手元へ回収します。VMに入るまでの操作は、各サービスの節でスクリーンショットとともに紹介します。

スクリプトを配置する

VMのターミナルでsurvey.shを作成します。次のコマンドを貼り付けると入力待ちになるので、上に掲載したsurvey.shの全文を貼り付け、最後にSURVEY_EOFとだけ書いた行を入力します。

VM内で実行
mkdir -p ~/agent-vm-survey && cd ~/agent-vm-survey
cat > survey.sh <<'SURVEY_EOF'

貼り付けた内容が本記事のものと同じかは、ハッシュ値で確認できます。49bfd5c83182f02d13ff2a9cc61bc897cdfd28a4aacbe2fd83070daa4b819a4aと表示されれば同じです。

VM内で実行
sha256sum survey.sh

各サービスの節のスクリーンショットは、survey.shなどをBase64でまとめた展開用スクリプトで配置したときのものです。そのため、チェックサムのOK表示やホームディレクトリ配下の展開先が写っています。

調査を実行する

survey.shの引数には、結果を区別するための名前を指定します。今回はdots、muse、grok-botを使い、dotsならbash survey.sh dotsのように実行します。

--planと--noteを使うと、契約プランやターミナルを開いた方法を手入力で記録できます。今回は両方を省略したため、結果の該当欄はunknownのままです。

ここで調べるのは、各サービスのクラウドVMです。手元のPCにも接続できるサービスでは、ターミナルを開いている場所を確認してから実行します。

結果を確認する

実行すると、results配下にサービス名と日時を含むディレクトリが作られます。まずsummary.mdで全体を確認し、気になる項目があればcommands.txtの出力を見ます。

結果を手元へ持ち帰るために、VM内で圧縮しておきます。dotsの場合は次のコマンドです。

VM内で調査結果を圧縮
tar -czf dots-results.tar.gz results/dots-*

Grok Bot

Grok Botのクラウドコンピューターには、ブラウザー、コマンドライン、ファイルシステムがあります。公式ドキュメントでは、同じアカウントのBot間でこのコンピューターを共有すると説明されています。

チャット上部のBot名をクリックします。今回は「テスト用」というBotを使います。

Grok Botのチャット上部にある「テスト用」のBot名をクリックする

右側に表示されたパネルで「コンピューター」を選びます。コンピューターの画面が表示され、この時点では「起動中...」となっていました。

右側の「コンピューター」タブを開き、コンピューターが起動中になっている画面

起動すると、コンピューターのデスクトップが表示されました。

起動が完了し、コンピューターのデスクトップが表示された画面

画面中央に表示される「開く」をクリックします。

コンピューターの画面中央にある「開く」ボタン

別ウィンドウでデスクトップが開きます。最初は「表示のみ」になっているので、画面下部の「操作を引き継ぐ」をクリックします。

デスクトップ画面下部の「操作を引き継ぐ」ボタン

下部の表示が「操作中」に変わりました。左下にあるターミナルのアイコンをクリックします。

操作を引き継いだ状態で、左下のターミナルアイコンをクリックする

ターミナルが開きました。作業ディレクトリは/workspaceです。

作業ディレクトリがworkspaceになっているVM内のターミナル

スクリプトをコピーして、ターミナルへ貼り付けます。今回は「Warning: Potentially Unsafe Paste」という確認画面が表示されました。貼り付ける内容を確認して「Paste」をクリックします。

スクリプトの貼り付け時に表示された確認画面とPasteボタン

貼り付け直後は、スクリプト末尾の表示が崩れて見えました。ここでEnterを押して実行します。

一括貼り付け後にスクリプト末尾の表示が崩れたターミナル

Enterを押すと、展開先へ移動するコマンドが表示されました。

Enterを押して展開が完了し、各ファイルのチェックサムと次に実行するコマンドが表示された画面

表示されたcdコマンドで展開先へ移動し、grok-botを指定して調査を実行します。今回は以下のディレクトリでした。

実行コマンド
cd /home/box/agent-vm-survey.eb6lkJ/agent-vm-survey
bash survey.sh grok-bot

調査が完了すると、結果の保存先が表示されました。

実行結果
Collecting grok-bot ...
Done: ./results/grok-bot-20261101T233632Z-vUjsis
Read summary.md; compare facts.tsv; evidence in commands.txt.

結果を手元のMacへ取り出すため、結果フォルダーを圧縮して/workspaceに保存します。以下のパスは今回の実行結果に合わせたものです。

実行コマンド
tar -czf /workspace/grok-bot-results-20261101T233632Z-vUjsis.tar.gz \
  -C /home/box/agent-vm-survey.eb6lkJ/agent-vm-survey/results \
  grok-bot-20261101T233632Z-vUjsis

調査スクリプトの実行が完了し、結果をworkspaceに圧縮して保存したターミナル

Grok Botのチャットに戻り、作成したファイルを添付してもらいます。

プロンプト
/workspace/grok-bot-results-20261101T233632Z-vUjsis.tar.gz を、そのままダウンロードできるファイルとしてこのチャットに添付してください。

13KBのファイルが添付されました。これをダウンロードすれば、手元で結果を確認できます。

Grok Botに圧縮ファイルの添付を依頼し、13KBのファイルが返ってきたチャット

ダウンロード後は、Macのターミナルでアーカイブを展開します。ここでは~/Downloadsに保存した場合のコマンドを載せます。

手元のMacで展開
mkdir -p "$HOME/agent-vm-survey/results"
tar -xzf "$HOME/Downloads/grok-bot-results-20261101T233632Z-vUjsis.tar.gz" \
  -C "$HOME/agent-vm-survey/results"
cd "$HOME/agent-vm-survey/results/grok-bot-20261101T233632Z-vUjsis"

展開先でチェックサムを確認します。Macではshasum -a 256を使います。

手元のMacでチェックサムを確認
shasum -a 256 -c SHA256SUMS
実行結果
survey.sh: OK
facts.tsv: OK
commands.txt: OK
summary.md: OK

調査結果

調査日時は2026年11月2日8時36分32秒(JST)です。macOS版Grok Botでクラウドコンピューターの操作を引き継ぎ、ターミナルから実行しました。

SHA256SUMSで4ファイルの一致を確認しました。実行されたsurvey.shは本記事に掲載したものと同じです。

項目 観測結果
論理CPU数 8(lscpu、getconf、nprocで一致)
プロセスに許可されたCPU 0-7
メモリ総量 約15.63GiB(16,779,640,832 bytes)
利用可能メモリ 約3.19GiB(/proc/meminfoのMemAvailable)
実行ユーザーのUID 1000

/、ホームディレクトリ、スクリプトの実行ディレクトリは同じファイルシステムでした。作業ディレクトリとして調べたのは、展開先の/home/box/agent-vm-survey.eb6lkJ/agent-vm-surveyです。

cgroupはv2でした。実行プロセスが属するグループでは、次の値を取得できました。

項目 値
cpu.max max 100000
cpuset.cpus.effective 0-7
memory.max max
pids.max max

このグループのCPU時間、メモリ、プロセス数には、数値による上限が設定されていませんでした。cgroupの制限は親階層からも適用されますが、親階層のcpu.maxやmemory.maxは取得できませんでした。

仮想化の確認では、lscpuにHypervisor vendor: KVM、Virtualization type: fullと表示されました。一方、systemd-detect-virtの結果はcontainer-otherでした。

systemd-detect-virtは、VMとコンテナーが併用されている場合、標準では内側のコンテナーを検出する仕様です。

GPUを確認するlspciとnvidia-smiは、どちらもcommand not foundでした。

python3は利用できましたが、pythonというコマンドは見つかりませんでした。chromium、chromium-browser、firefoxも、今回のターミナルでは見つかっていません。

dots

dotsには、dot自身のファイルやソフトウェア、ブラウザーセッションを持つクラウドコンピューターがあります。公式ドキュメントでは、このコンピューターの操作を引き継ぐ方法が紹介されています。

画面右上の、赤枠で示したアイコンをクリックします。

dotsの画面右上にある情報パネルを開くアイコン

右側にdotの情報パネルが表示されました。

dotの情報とクラウドコンピューターが表示された右側のパネル

「あなたの dot のコンピューター」をクリックします。今回は「利用可能」と表示されていました。

情報パネルの「あなたの dot のコンピューター」をクリックする

コンピューターの画面がタブで開きます。画面下部の「操作を引き継ぐ」をクリックし、続けて下端にあるターミナルのアイコンをクリックします。

dotのコンピューターを開いた画面と操作を引き継ぐボタン、ターミナルのアイコン

ターミナルが開きました。下部には「あなたに操作権があります」と表示され、作業ディレクトリは/workspaceになっています。

操作権を引き継ぎ、workspaceでターミナルを開いた画面

スクリプトをdotsのターミナルへ貼り付けると、「警告: 安全ではない可能性のある貼り付け」という確認画面が表示されました。内容を確認して「貼り付け」をクリックし、Enterを押して実行します。

dotsのターミナルでスクリプトを貼り付けた際の確認画面

展開先へ移動して調査を実行します。今回は以下のディレクトリです。

dotsのVM内で調査
cd /home/agent/agent-vm-survey.clUdmP/agent-vm-survey
bash survey.sh dots

調査が完了し、結果の保存先が表示されました。

実行結果
Collecting dots ...
Done: ./results/dots-20261102T000124Z-RU6YPb
Read summary.md; compare facts.tsv; evidence in commands.txt.

スクリプトの展開とdotsの調査が完了し、結果の保存先が表示されたターミナル

展開先のディレクトリにいる状態で、結果を圧縮します。調査日時を含むディレクトリ名はそのまま残し、results/dots-*に一致する結果をまとめます。

dotsのVM内で結果を圧縮
tar -czf /tmp/dots-results.tar.gz results/dots-*

dotsの調査結果をtmpに圧縮して保存したターミナル

dotsのチャットに戻り、圧縮したファイルの添付を依頼しました。

プロンプト
あなたのクラウドコンピューターにある /tmp/dots-results.tar.gz を、そのままダウンロードできるファイルとしてこのチャットに添付してください。

最初はファイルが見つからないと返ってきましたが、「あるはずです」と返すと、dotsがターミナルを操作して再確認してくれました。スクリーンショットの赤線部分は、このときdotsが実行したコマンドです。

dotsが実行したコマンド
ls -l /tmp/dots-results.tar.gz && cp /tmp/dots-results.tar.gz /workspace/shared/dots-results.tar.gz

/workspace/sharedへコピーした後、dots-results.tar.gzをチャットに添付してくれました。

最初はファイルが見つからず、再確認したdotsがターミナルを操作してファイルを添付したやりとり

ファイルをMacの~/Downloadsに保存したら、次のコマンドで展開します。このアーカイブにはresultsディレクトリも含めているため、展開先は~/agent-vm-surveyです。

手元のMacで展開
mkdir -p "$HOME/agent-vm-survey"
tar -xzf "$HOME/Downloads/dots-results.tar.gz" \
  -C "$HOME/agent-vm-survey"
cd "$HOME/agent-vm-survey"

今回の結果ディレクトリへ移動し、チェックサムを確認します。

手元のMacでチェックサムを確認
cd "$HOME/agent-vm-survey/results/dots-20261102T000124Z-RU6YPb"
shasum -a 256 -c SHA256SUMS

4ファイルともチェックサムが一致しました。

実行結果
survey.sh: OK
facts.tsv: OK
commands.txt: OK
summary.md: OK

調査結果

調査日時は2026年11月2日9時1分24秒(JST)です。macOS版dotsでクラウドコンピューターの操作を引き継ぎ、ターミナルから実行しました。

実行されたsurvey.shは本記事に掲載したものと同じです。

項目 観測結果
論理CPU数 9(lscpu、getconf、nprocで一致)
プロセスに許可されたCPU 0-8
メモリ総量 約9.73GiB(10,451,427,328 bytes)
利用可能メモリ 約8.56GiB(/proc/meminfoのMemAvailable)
実行ユーザーのUID 1000

ファイルシステムは、ルートとホームディレクトリで異なっていました。

調査対象 種別の表示 容量 空き容量
/ tmpfs 約4.87GiB 約4.87GiB
/home/agent overlayfs 約62.94GiB 約59.57GiB
スクリプトの実行ディレクトリ overlayfs(ホームと同じファイルシステム) 約62.94GiB 約59.57GiB

ホームと実行ディレクトリは同じファイルシステムです。作業ディレクトリとして調べたのは、展開先の/home/agent/agent-vm-survey.clUdmP/agent-vm-surveyです。

cgroupについては、スクリプトがプロセスの所属先とマウントを対応付けられず、cgroup.hierarchies_mappedは0でした。

仮想化の確認では、Grok Botと同じくlscpuにHypervisor vendor: KVM、Virtualization type: fullと表示され、systemd-detect-virtの結果はcontainer-otherでした。GPUを確認するlspciとnvidia-smiは、どちらもcommand not foundでした。

今回のターミナルでは、nodeとnpmは見つかりませんでした。python、chromium-browser、google-chrome、firefoxもcommand not foundになっています。

Muse

Museは、ユーザーと共有する専用のクラウドコンピューターを持っています。Metaの説明では、Muse Secure VMと呼ばれる環境で動作します。

VMにSSHで入る

MuseのVMには、公式のターミナルやSSHの入口がありません。インバウンド経路がなく、sshdも入っていないためです。そこで、Qiitaの記事を下敷きに、VMの内側からPinggyへリバーストンネルを張り、ループバック限定で立てたsshdに接続する方法を採ります。一時的にシェルへ入るための方法で、恒常運用はしません。

項目 内容
sshd OpenSSH 9.6p1(Ubuntu 24.04用.debを展開して使用。システムへはインストールしない)
sshd待ち受け 127.0.0.1:2222のみ、公開鍵認証のみ、パスワード認証オフ
公開 Pinggy無料トンネル(TCP)。60分で失効し、張るたびにアドレスが変わる使い捨て前提
鍵 Macで生成した使い捨ての鍵ペア。秘密鍵はMacから出さない。VMへ渡すのは公開鍵だけ

2026年11月2日に、OpenSSH_9.6p1 Ubuntu-3ubuntu13.20でsshdを起動し、ローカルでの疎通確認から、Pinggy経由でMacからSSH接続するところまで確認できました。以下の画像と接続ログでは、接続先のホスト名、IPアドレス、割り当てられたポート、鍵のフィンガープリント、VMのホスト名を伏せています。

手順1(Mac):使い捨て鍵を作る

手元のMacで実行
ssh-keygen -t ed25519 -f ~/.ssh/muse-vm-once -C muse-vm-once -N ''
cp ~/.ssh/muse-vm-once.pub ~/muse-vm-once.pub

公開鍵はチャットに貼る必要はありません。~/.ssh配下はエージェントから読めないため、公開鍵のコピーだけをホーム直下の~/muse-vm-once.pubに置き、手順2でMuseに読み取らせます。秘密鍵~/.ssh/muse-vm-onceはコピーも送信もしないでください。

この例では、使い捨て鍵をパスフレーズなし(-N '')で作成します。使い回さず、手順6で片付けます。

手順2(VM):sshdを用意してローカルで疎通確認する

VMにはターミナルがないため、この手順はMuseのチャットへ送る依頼文として用意します。差し替えは不要で、全文をそのまま送ります。

Museへ送る依頼文
MuseのVMで、SSH接続を受ける準備をしてください。

- 公開鍵は私のMacの`~/muse-vm-once.pub`にあります。まずそれを読み取ってください。私にチャットへ貼らせることはしないでください
- 次のスクリプトをそのまま実行してください。失敗したら、エラーの出力をそのまま伝えて止めてください
- スクリプトの実行後、読み取った公開鍵を`/root/.ssh/authorized_keys`に書き込み、`chmod 600 /root/.ssh/authorized_keys`を実行してください。そのうえで、スクリプト後半のローカル疎通確認へ進んでください
- 実行したら、ホスト鍵のフィンガープリント(`SHA256:...`)と、後半のローカル疎通確認で`LOGIN_OK`が返ったかを報告してください
- Pinggyのトンネルはまだ開かないでください

set -eu
BASE="$HOME/workspace/muse-ssh-once"
VER="9.6p1-3ubuntu13.20"
mkdir -p "$BASE"/{debs,rootfs,hostkeys}

# aptはプロキシで失敗するため、.debをcurlで直接取得して展開する
cd "$BASE/debs"
curl -fsSLO "http://archive.ubuntu.com/ubuntu/pool/main/o/openssh/openssh-server_${VER}_amd64.deb"
curl -fsSLO "http://archive.ubuntu.com/ubuntu/pool/main/o/openssh/openssh-sftp-server_${VER}_amd64.deb"
dpkg-deb -x "openssh-server_${VER}_amd64.deb" "$BASE/rootfs"
dpkg-deb -x "openssh-sftp-server_${VER}_amd64.deb" "$BASE/rootfs"
"$BASE/rootfs/usr/sbin/sshd" -V

# 特権分離用ユーザーと /run/sshd(無いとsshdが起動を拒否する)
id sshd >/dev/null 2>&1 || useradd --system --no-create-home --home-dir /run/sshd --shell /usr/sbin/nologin sshd
mkdir -p /run/sshd
chown root:root /run/sshd
chmod 755 /run/sshd

# authorized_keysはスクリプトの後でMuseが書き込む(置き場所の理由は後述のつまずき参照)
install -d -m 700 /root/.ssh

# ホスト鍵
if [ ! -f "$BASE/hostkeys/ssh_host_ed25519_key" ]; then
  ssh-keygen -t ed25519 -f "$BASE/hostkeys/ssh_host_ed25519_key" -N '' -C muse-vm-once
fi

# sshd設定(ループバック限定、公開鍵のみ)
cat > "$BASE/sshd_config" <<EOF
Port 2222
ListenAddress 127.0.0.1
HostKey $BASE/hostkeys/ssh_host_ed25519_key
PidFile $BASE/sshd.pid
PermitRootLogin prohibit-password
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM no
AllowTcpForwarding local
X11Forwarding no
Subsystem sftp $BASE/rootfs/usr/lib/openssh/sftp-server
EOF

"$BASE/rootfs/usr/sbin/sshd" -t -f "$BASE/sshd_config"
"$BASE/rootfs/usr/sbin/sshd" -f "$BASE/sshd_config" -E "$BASE/sshd.log"
ss -tln | grep 2222
ssh-keygen -lf "$BASE/hostkeys/ssh_host_ed25519_key.pub"

# ここからローカル疎通確認(テスト鍵は確認後に外す)
BASE="$HOME/workspace/muse-ssh-once"
ssh-keygen -t ed25519 -f "$BASE/testkey" -N '' -C test-only
cat "$BASE/testkey.pub" >> /root/.ssh/authorized_keys
ssh -i "$BASE/testkey" -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@127.0.0.1 'echo LOGIN_OK; hostname; pwd'
# テスト鍵はトンネルを開く前に外す
sed -i '/test-only$/d' /root/.ssh/authorized_keys
rm -f "$BASE/testkey" "$BASE/testkey.pub"

MuseからフィンガープリントとLOGIN_OKの報告が返れば、VM側の準備は完了です。フィンガープリントは手順4での照合用に控えます。

今回も、127.0.0.1:2222で待ち受けていることと、ローカルのSSH接続でLOGIN_OKが返ったことが報告されました。この時点では、Pinggyのトンネルはまだ開いていません。

Museからsshdの起動とローカル疎通確認の結果が返る

つまずき:authorized_keysをワークスペースに置くと認証が拒否される

AuthorizedKeysFileで~/workspace配下のファイルを指定すると、ログインがPermission denied (publickey)になります。sshdのログには次が残ります。

Authentication refused: bad ownership or modes for directory /home/hatch/workspace

~/workspaceがグループ書き込み可能(drwxrws---)なため、StrictModesの検査に引っかかります。$BASEだけをchmod 700にしても、親ディレクトリで同じ拒否が起きます。このため公開鍵は、伝統どおり/root/.ssh/authorized_keys(ディレクトリ700、ファイル600)に置きます。

手順3(VM):Pinggyトンネルを開く

ここから先は外部公開になるため、実施のゴーサインを出してから、Museに次の依頼文を送ります。

Museへ送る依頼文
手順2で起動したsshd(127.0.0.1:2222)へ、Pinggyの無料トンネルを開いてください。

ssh -p 443 -o StrictHostKeyChecking=no -o ServerAliveInterval=30 -R0:localhost:2222 tcp@a.pinggy.io

トンネルはバックグラウンドで維持し、出力に表示された接続先のホストとポートだけを報告してください。接続先は私以外に共有しないでください。

最初の試行ではトンネルを開けませんでした。Museから、sshdは正常に待ち受けているものの、外向きのTCP接続がプロキシでブロックされていると返ってきました。案内されたのは、other_tcpをDenyからAskに変更する設定です。

Pinggyへの接続が拒否され、Museからネットワーク権限の設定変更を案内された画面。プロキシのIPアドレスとポートは黒塗り

Museの設定で「アクセス許可」から「ダイレクトネットワークプロトコル」を開き、「その他のTCP接続」をオンにします。

Museの設定でその他のTCP接続をオンにする

設定を変更して「許可しました」と伝えると、a.pinggy.ioへの接続を許可するか確認されました。「一度だけ許可」で、今回のトンネル接続を許可します。

a.pinggy.ioへの接続許可を求める画面

許可後はトンネルが開き、Museから接続先のホストとポートが返ってきました。

Museからトンネルの接続先が返る。ホストとポートは黒塗り

報告された接続先<ホスト>:<ポート>が、その回限りの接続先です。トンネルは無料枠のため、開いてから60分で失効します。

You are not authenticated.
Your tunnel will expire in 60 minutes.

手順4(Mac):接続する

Museから返ってきたホストとポートを、Macの環境変数に設定します。以下の<ホスト>と<ポート>を自分の接続先に置き換えます。

手元のMacで接続情報を設定
export MUSE_SSH_HOST='<ホスト>'
export MUSE_SSH_PORT='<ポート>'
export MUSE_SSH_KEY="$HOME/.ssh/muse-vm-once"
手元のMacで実行
ssh -i "$MUSE_SSH_KEY" -p "$MUSE_SSH_PORT" "root@$MUSE_SSH_HOST"

初回接続時に表示されるフィンガープリントが、手順2で控えたものと一致することを確認してから承諾します。ログイン後は/rootに着地します。

実際の接続ログは以下です。初回の確認にyesを入力すると、VMのrootシェルに入れました。

SSH接続結果(接続情報は伏せ字)
$ ssh -i ~/.ssh/muse-vm-once -p <ポート> root@<ホスト>
The authenticity of host '[<ホスト>]:<ポート> ([<IPアドレス>]:<ポート>)' can't be established.
ED25519 key fingerprint is: SHA256:<フィンガープリント>
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[<ホスト>]:<ポート>' (ED25519) to the list of known hosts.
root@<VMのホスト名>:~#

手順5(VM):調査を実行して結果を回収する

ログインしたターミナルは、そのままVMのターミナルとして使えます。スクリプトを配置したら、展開先へcdしてから調査を実行します。

VMで実行
export MUSE_SURVEY_DIR='/root/agent-vm-survey.GxJH21/agent-vm-survey'
cd "$MUSE_SURVEY_DIR"
bash survey.sh muse

今回の実行結果は以下です。

実行結果
Collecting muse ...
Done: ./results/muse-20261102T003029Z-uq7EGT
Read summary.md; compare facts.tsv; evidence in commands.txt.

続いて、実行結果のディレクトリ名を環境変数に設定し、圧縮します。lsで圧縮ファイルが作成されたことを確認してから、Mac側へ移ります。

VMで結果を圧縮
export MUSE_RESULT_NAME='muse-20261102T003029Z-uq7EGT'
export MUSE_ARCHIVE="$MUSE_SURVEY_DIR/muse-results.tar.gz"
tar -czf "$MUSE_ARCHIVE" \
  -C "$MUSE_SURVEY_DIR/results" "$MUSE_RESULT_NAME" &&
  ls -lh "$MUSE_ARCHIVE"

Macの別ターミナルで、手順4の接続情報の設定をもう一度実行します。VM側で設定した環境変数はMacには引き継がれないため、VMの作業ディレクトリとMacの保存先も設定してからscpを実行します。

手元のMacで結果を回収
export MUSE_SURVEY_DIR='/root/agent-vm-survey.GxJH21/agent-vm-survey'
export MUSE_DOWNLOAD_DIR="$HOME/agent-vm-survey"
mkdir -p "$MUSE_DOWNLOAD_DIR"
scp -i "$MUSE_SSH_KEY" -P "$MUSE_SSH_PORT" \
  "root@${MUSE_SSH_HOST}:${MUSE_SURVEY_DIR}/muse-results.tar.gz" \
  "$MUSE_DOWNLOAD_DIR/"

転送が完了しました。

転送結果
muse-results.tar.gz                                             100%   12KB  18.5KB/s   00:00

取得したアーカイブをMacで展開し、チェックサムを確認します。MUSE_RESULT_NAMEには、VMで表示された結果ディレクトリ名を設定します。

手元のMacで展開してチェックサムを確認
export MUSE_RESULT_NAME='muse-20261102T003029Z-uq7EGT'
mkdir -p "$MUSE_DOWNLOAD_DIR/results"
tar -xzf "$MUSE_DOWNLOAD_DIR/muse-results.tar.gz" -C "$MUSE_DOWNLOAD_DIR/results"
cd "$MUSE_DOWNLOAD_DIR/results/$MUSE_RESULT_NAME"
shasum -a 256 -c SHA256SUMS

4ファイルともチェックサムが一致しました。

実行結果
survey.sh: OK
facts.tsv: OK
commands.txt: OK
summary.md: OK

手順6:片付け

Mac側はexitで切断します。VM側はMuseに次の依頼文を送ります。

Museへ送る依頼文
SSHの後片付けをしてください。Pinggyのトンネルとsshdを止め、`/root/.ssh/authorized_keys`を削除して、ポート2222が閉じたことを確認してください。

BASE="$HOME/workspace/muse-ssh-once"
kill "$(cat "$BASE/sshd.pid")"
rm -f /root/.ssh/authorized_keys
ss -tln | grep 2222 || echo "2222 closed"

トンネルの期限切れを待たず、結果の回収が終わった時点で片付けます。Mac側の使い捨て鍵が不要になれば~/.ssh/muse-vm-onceと~/.ssh/muse-vm-once.pub、公開鍵のコピー~/muse-vm-once.pubを削除します。今回の接続のために変更した「その他のTCP接続」の設定も、元へ戻します。

調査結果

調査日時は2026年11月2日9時30分29秒(JST)です。MuseにSSH用のsshdとPinggyトンネルを用意してもらい、MacからSSHで接続したrootシェルで実行しました。

実行されたsurvey.shは本記事に掲載したものと同じです。

項目 観測結果
論理CPU数 4(lscpu、getconf、nprocで一致)
プロセスに許可されたCPU 0-3
メモリ総量 約7.75GiB(8,319,901,696 bytes)
利用可能メモリ 約0.46GiB(/proc/meminfoのMemAvailable)
実行ユーザーのUID 0

/、ホームの/root、スクリプトの実行ディレクトリは同じoverlayfsでした。今回調べた実行ディレクトリは/root/agent-vm-survey.GxJH21/agent-vm-surveyです。

cgroupはv2で、実行プロセスの所属先をマウントと対応付けられました。ただし、読み取れたのは現グループのcpu.statで、cpu.max、memory.max、pids.maxなどは存在しないか読み取れない状態でした。

仮想化に関する出力は、以下のとおりでした。

確認項目 出力
lscpuのHypervisor vendor KVM
lscpuのVirtualization type full
systemd-detect-virt systemd-nspawn
DMIのsys_vendor Cloud Hypervisor
DMIのproduct_name cloud-hypervisor

Grok BotとdotsではDMIの値を読み取れませんでしたが、MuseではCloud Hypervisorという表示を確認できました。

GPUを確認するlspciとnvidia-smiは、どちらもcommand not foundでした。

Node.jsは使えましたが、npmは見つかりませんでした。python、chromium、chromium-browser、google-chrome、firefoxも、今回SSHで入ったシェルでは見つかっていません。

感想

スペックの印象

個人的には、Grok Botの約16GiBというメモリ容量は大きめに感じました。ただ、調査時に利用可能だったのは総量の約20%だったので、積みすぎているのかは分かりません。

最も小さい構成のMuseは、個人利用を意識して作業環境をコンパクトにまとめているように感じました。ただ、利用可能メモリが約0.46GiBだったので、効率よく収まっているのか、余裕が少ないのかは分かりません。

必要なメモリ量を見極めるなら、待機中と作業中の変化や、プロセスごとの内訳まで追ってみたいところです。

近い構成をEC2で動かすといくらか

近いCPU数とメモリ容量をEC2で用意した場合の費用も計算してみました。Grok Botの提供コストが収益に対して高すぎたというElon Musk氏の投稿を見て気になりました。

https://x.com/elonmusk/status/2116992258520064397

条件は、東京リージョン(ap-northeast-1)、Linux、共有テナンシーのオンデマンド料金で、月730時間の稼働とします。料金は2026年11月2日にAWS公式の料金データで確認し、割引、無料枠、税金は含めません。

インスタンスはx86_64のC7iから容量の近いサイズを選びました[9]。dotsの9 CPU/約9.73GiBに一致するサイズはないため、CPUが1個少ないc7i.2xlargeで近似しています。

比較対象 EC2の構成 EC2のみ/時間 EC2のみ/月 EBSとIPv4込み/月
Grok Bot c7i.2xlarge(8 vCPU/16GiB) $0.4494 $328.06 $344.00
dots(CPU数が1個少ない近似) c7i.2xlarge(8 vCPU/16GiB) $0.4494 $328.06 $337.86
Muse c7i.xlarge(4 vCPU/8GiB) $0.2247 $164.03 $168.45

1ドル150円で換算すると、EBSとIPv4込みでGrok Botは月約5.16万円、dotsの近似は約5.07万円、Museは約2.53万円です[10]。データ転送、バックアップ、LLMの推論費用などは含みません。

試算の内訳

月額は、EC2の時間単価×730時間に、EBSとパブリックIPv4の費用を加えています。

費目 単価 Grok Botの例 dotsの近似 Museの例
EC2 上の表の時間単価 $328.062 $328.062 $164.031
EBS gp3の容量 $0.096/GB月 128GiBで$12.288 64GiBで$6.144 8GiBで$0.768
パブリックIPv4 $0.005/時間 $3.65 $3.65 $3.65
合計(小数第2位に丸め) $344.00 $337.86 $168.45

EBSはAWS公式のEBS料金データのStorage General Purpose gp3 GB Mo、IPv4はAmazon VPCの料金を参照しました。gp3は標準のIOPSとスループットの範囲内とし、追加性能の料金は含めていません。

EC2とEBSの料金データの公開日時は2026年10月29日17:56:34 UTCでした。リンク先は更新されるため、ここに記載した単価は確認日時点のものです。

dotsの9 CPUを下回らないc7i.4xlarge(16 vCPU/32GiB)にすると、EBSとIPv4込みで月$665.92です。CPUもメモリも大きく上回るため、dots相当の費用とはいえません。

上の金額は常時稼働させた場合です。使っていない間はVMがスリープするので、稼働時間に応じて課金されるEC2[11]に置き換えれば、実際はもっと安くなるはずです。ただ、定期実行のバッチを組めばほぼ常時動かすことになります。

料金はGrok Botが月$30、dotsが月16,800円、Museは0円です。リソースの専有状況や事業者の契約単価は分からないので、この試算だけで原価は判断できません。

Grok Botについては、上の投稿のとおり、少なくとも現段階では持続可能ではないと提供側が判断しています。この投稿は利用枠がすぐなくなるという声への返信で、費用を理由に利用枠を絞ったと読めます。ただ、VMと推論のどちらの費用が重いのかまでは書かれていません。

Grok Botの無料試用のクレジットは、メッセージ数ではなく、エージェントのステップ数とトークン数で消費されると説明されています[12]。推論の量で測っているので、推論の費用が効いているようにも見えます。一方で、エージェントが動いている間はVMも動くので、利用枠を絞ればVMの稼働時間も減ります。今回の試算だけでは、どちらが主な理由なのかは判断が難しいところです。

脚注
  1. いずれもエージェントを利用するための料金で、VMだけを借りる料金ではありません。 ↩︎

  2. Grok BotのPremiumは、Grok、Cursor、Xをまとめた「The X Subscription」のプランで、Grok BotはPremium以上に含まれます。従来のX Premiumとは別の料金体系なので、統合後のプラン名を記載しています。利用中のプランがPremiumであることは、Grok Dashboardでも確認しました。The X Subscriptionの公式料金表、Grok Botの対象プラン ↩︎

  3. dotsはChatGPT Proに含まれます。公式ドキュメントではPro 100は月$100と案内されていますが、ここではWeb版の「設定 → 請求 → プランを変更」に表示された日本円の料金(2026年11月2日時点、JCT込み)を載せています。dotsの対象プラン、ChatGPT Proの公式ドル料金 ↩︎

  4. Metaの公式案内では、Museは無料で利用でき、より多く使う人向けに有料プランも用意されていると説明されています。MetaによるMuseの公式案内 ↩︎

  5. CPUモデル名の80-Coreや126-Coreは、各環境に割り当てられたCPU数ではありません。 ↩︎

  6. MemAvailableは、回収可能なキャッシュなども考慮した、スワップせずに新しいアプリケーションに使えるメモリ量の推定値です。Linuxカーネルの説明(/proc/meminfo) ↩︎

  7. memory.currentは、そのcgroupと子孫が使っているメモリ量です。cgroup v2の説明(memory.current) ↩︎

  8. 表の「見つからず」は、調査時のターミナルでそのコマンドを実行できなかったという意味です。 ↩︎

  9. CPUの世代や物理コアとの対応が異なるので、処理性能が同じという意味ではありません。 ↩︎

  10. EC2の時間単価はAWS公式のEC2料金データの値です。EBS(gp3)とパブリックIPv4はホームの容量に合わせた仮定で、各サービスの実際の構成とは関係ありません。 ↩︎

  11. EC2の課金条件 ↩︎

  12. 無料試用の説明に「measured by agent steps and tokens rather than the number of messages you send」とあります。Grok BotのFAQ ↩︎

この記事をシェアする

関連記事