
Grok Bot、dots、MuseのVM構成を比較する
はじめに
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で動作し、追加のパッケージをインストールせずに実行できます。
スクリプトの全体は以下です。
調査スクリプト
#!/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(/&/, "\\&", $i); gsub(/</, "\\<", $i); gsub(/\|/, "\\|", $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とだけ書いた行を入力します。
mkdir -p ~/agent-vm-survey && cd ~/agent-vm-survey
cat > survey.sh <<'SURVEY_EOF'
貼り付けた内容が本記事のものと同じかは、ハッシュ値で確認できます。49bfd5c83182f02d13ff2a9cc61bc897cdfd28a4aacbe2fd83070daa4b819a4aと表示されれば同じです。
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の場合は次のコマンドです。
tar -czf dots-results.tar.gz results/dots-*
Grok Bot
Grok Botのクラウドコンピューターには、ブラウザー、コマンドライン、ファイルシステムがあります。公式ドキュメントでは、同じアカウントのBot間でこのコンピューターを共有すると説明されています。
チャット上部のBot名をクリックします。今回は「テスト用」というBotを使います。

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

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

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

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

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

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

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

貼り付け直後は、スクリプト末尾の表示が崩れて見えました。ここで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

Grok Botのチャットに戻り、作成したファイルを添付してもらいます。
/workspace/grok-bot-results-20261101T233632Z-vUjsis.tar.gz を、そのままダウンロードできるファイルとしてこのチャットに添付してください。
13KBのファイルが添付されました。これをダウンロードすれば、手元で結果を確認できます。

ダウンロード後は、Macのターミナルでアーカイブを展開します。ここでは~/Downloadsに保存した場合のコマンドを載せます。
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を使います。
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自身のファイルやソフトウェア、ブラウザーセッションを持つクラウドコンピューターがあります。公式ドキュメントでは、このコンピューターの操作を引き継ぐ方法が紹介されています。
画面右上の、赤枠で示したアイコンをクリックします。

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

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

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

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

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

展開先へ移動して調査を実行します。今回は以下のディレクトリです。
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.

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

dotsのチャットに戻り、圧縮したファイルの添付を依頼しました。
あなたのクラウドコンピューターにある /tmp/dots-results.tar.gz を、そのままダウンロードできるファイルとしてこのチャットに添付してください。
最初はファイルが見つからないと返ってきましたが、「あるはずです」と返すと、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をチャットに添付してくれました。

ファイルをMacの~/Downloadsに保存したら、次のコマンドで展開します。このアーカイブにはresultsディレクトリも含めているため、展開先は~/agent-vm-surveyです。
mkdir -p "$HOME/agent-vm-survey"
tar -xzf "$HOME/Downloads/dots-results.tar.gz" \
-C "$HOME/agent-vm-survey"
cd "$HOME/agent-vm-survey"
今回の結果ディレクトリへ移動し、チェックサムを確認します。
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):使い捨て鍵を作る
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の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のトンネルはまだ開いていません。

つまずき: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に次の依頼文を送ります。
手順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に変更する設定です。

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

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

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

報告された接続先<ホスト>:<ポート>が、その回限りの接続先です。トンネルは無料枠のため、開いてから60分で失効します。
You are not authenticated.
Your tunnel will expire in 60 minutes.
手順4(Mac):接続する
Museから返ってきたホストとポートを、Macの環境変数に設定します。以下の<ホスト>と<ポート>を自分の接続先に置き換えます。
export MUSE_SSH_HOST='<ホスト>'
export MUSE_SSH_PORT='<ポート>'
export MUSE_SSH_KEY="$HOME/.ssh/muse-vm-once"
ssh -i "$MUSE_SSH_KEY" -p "$MUSE_SSH_PORT" "root@$MUSE_SSH_HOST"
初回接続時に表示されるフィンガープリントが、手順2で控えたものと一致することを確認してから承諾します。ログイン後は/rootに着地します。
実際の接続ログは以下です。初回の確認にyesを入力すると、VMのrootシェルに入れました。
$ 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してから調査を実行します。
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側へ移ります。
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を実行します。
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で表示された結果ディレクトリ名を設定します。
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に次の依頼文を送ります。
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氏の投稿を見て気になりました。
条件は、東京リージョン(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の稼働時間も減ります。今回の試算だけでは、どちらが主な理由なのかは判断が難しいところです。
いずれもエージェントを利用するための料金で、VMだけを借りる料金ではありません。 ↩︎
Grok BotのPremiumは、Grok、Cursor、Xをまとめた「The X Subscription」のプランで、Grok BotはPremium以上に含まれます。従来のX Premiumとは別の料金体系なので、統合後のプラン名を記載しています。利用中のプランがPremiumであることは、Grok Dashboardでも確認しました。The X Subscriptionの公式料金表、Grok Botの対象プラン ↩︎
dotsはChatGPT Proに含まれます。公式ドキュメントではPro 100は月$100と案内されていますが、ここではWeb版の「設定 → 請求 → プランを変更」に表示された日本円の料金(2026年11月2日時点、JCT込み)を載せています。dotsの対象プラン、ChatGPT Proの公式ドル料金 ↩︎
Metaの公式案内では、Museは無料で利用でき、より多く使う人向けに有料プランも用意されていると説明されています。MetaによるMuseの公式案内 ↩︎
CPUモデル名の
80-Coreや126-Coreは、各環境に割り当てられたCPU数ではありません。 ↩︎MemAvailableは、回収可能なキャッシュなども考慮した、スワップせずに新しいアプリケーションに使えるメモリ量の推定値です。Linuxカーネルの説明(/proc/meminfo) ↩︎memory.currentは、そのcgroupと子孫が使っているメモリ量です。cgroup v2の説明(memory.current) ↩︎表の「見つからず」は、調査時のターミナルでそのコマンドを実行できなかったという意味です。 ↩︎
CPUの世代や物理コアとの対応が異なるので、処理性能が同じという意味ではありません。 ↩︎
EC2の時間単価はAWS公式のEC2料金データの値です。EBS(gp3)とパブリックIPv4はホームの容量に合わせた仮定で、各サービスの実際の構成とは関係ありません。 ↩︎
無料試用の説明に「measured by agent steps and tokens rather than the number of messages you send」とあります。Grok BotのFAQ ↩︎









