84e2a9f0ac
Server mode can now run each account as its own Linux user (ah-<name>) when `agenthub server install-system` has set S3 up. The dashboard reaches a member only through sudo -u / write-as / pane-launch, reads a member's files only through owner- and type-checked opens, and hooks are authenticated by peer uid. Local mode and server mode without S3 are unchanged. EXPERIMENTAL: tested with fake sudo and fake accounts only. It has not yet been verified on a real install with real member accounts, sudo or a real Codex login. Do not offer it to real users yet. - S3 core: provisioning, per-member tmux servers, the proxy key gateway, shared tools in /opt, dropped capabilities for children (P0–P5) - P4: plan and quota per member home - P6: ~/.claude.json seed; opt-in plugin baseline installed as the member; managed-settings spike script (not run) - P7: Codex account pool (`agenthub accounts add --tool codex`) - Roles merged with main's admin / analyst / developer: analysts write only their own rows, under S3 as well - Per-account prefs: one implementation (Chat UI + onboarding) - Security reviews F1–F49 (five rounds), each fix with a regression test: symlink/TOCTOU-safe member reads, no host writes in a member home, credentials and the data dir never served, owner and folder account must agree, pane tokens bound to the pane's uid, bounded sudo/tmux/git calls and member reads (no hang or crash from a member), async route rejections answer 500 instead of exiting - Docs: docs/SERVER-MODE.md (S3 model, accepted ceilings, operator runbook), docs/plans/server-mode/S3-PLAN.md, CLAUDE.md S3 rules Tests: server 2838/2841 (3 need 127.0.0.1:3000, held on this box); client 1162/1162; tsc clean; headers check passes. Co-Authored-By: Claude <noreply@anthropic.com>
130 lines
6.2 KiB
Bash
130 lines
6.2 KiB
Bash
#!/bin/bash
|
|
# @file spike-managed-plugins.sh
|
|
# @description S3 plan, P6 spike — run BY HAND, as root, never by AgentHub:
|
|
# does /etc/claude-code/managed-settings.json alone (`extraKnownMarketplaces`
|
|
# + `enabledPlugins`) give a brand-new Linux user the admin's plugins, with no
|
|
# `claude plugin install` run as that user?
|
|
#
|
|
# sudo bash spike-managed-plugins.sh run <fragment.json> [seconds]
|
|
# sudo bash spike-managed-plugins.sh undo
|
|
#
|
|
# run: backs up managed-settings.json (once), merges the fragment's two keys
|
|
# into it, makes the throwaway user agh-spike (NOT an AgentHub member),
|
|
# seeds its onboarding like agenthub-provision, lists its marketplaces
|
|
# and plugins, starts one interactive `claude` in a private tmux socket
|
|
# for [seconds] (default 45), lists again, and prints a verdict per
|
|
# enabled plugin. Everything lands in /var/tmp/agenthub-spike-managed/.
|
|
# undo: puts managed-settings.json back exactly (or removes it when there was
|
|
# none), ends agh-spike's processes and deletes that user and its home.
|
|
#
|
|
# While it is applied, EVERY Claude Code on this machine (the host's own
|
|
# sessions too) reads the merged file. Run it when that is acceptable and
|
|
# undo right after. It never touches /etc/agenthub or an ah-* account.
|
|
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
set -euo pipefail
|
|
|
|
die() { echo "spike: $*" >&2; exit 1; }
|
|
[ "$(id -u)" = 0 ] || die "run as root (sudo bash $0 …)"
|
|
|
|
MANAGED=/etc/claude-code/managed-settings.json
|
|
BACKUP=$MANAGED.agenthub-spike-bak
|
|
NONE=$MANAGED.agenthub-spike-none # marker: there was no file before
|
|
OUT=/var/tmp/agenthub-spike-managed
|
|
U=agh-spike
|
|
CLAUDE=/opt/agenthub/tools/bin/claude
|
|
NODE=/opt/agenthub/tools/bin/node
|
|
|
|
as_user() {
|
|
local h
|
|
h=$(getent passwd "$U" | cut -d: -f6)
|
|
(cd / && runuser -u "$U" -- env -i HOME="$h" USER="$U" LOGNAME="$U" TERM=xterm-256color \
|
|
PATH=/opt/agenthub/tools/bin:/usr/local/bin:/usr/bin:/bin DISABLE_AUTOUPDATER=1 "$@")
|
|
}
|
|
|
|
case "${1:-}" in
|
|
run)
|
|
FRAG=${2:-}
|
|
SECS=${3:-45}
|
|
[ -f "$FRAG" ] || die "usage: $0 run <fragment.json> [seconds]"
|
|
[[ "$SECS" =~ ^[0-9]+$ ]] || die "seconds: a number"
|
|
[ -x "$CLAUDE" ] && [ -x "$NODE" ] || die "$CLAUDE or $NODE missing (agenthub server install-system)"
|
|
! id "$U" >/dev/null 2>&1 || die "$U already exists — run '$0 undo' first"
|
|
install -d -m 0700 "$OUT"
|
|
|
|
# 1. Back up once; a second run never overwrites the first backup.
|
|
install -d -m 0755 /etc/claude-code
|
|
if [ ! -e "$BACKUP" ] && [ ! -e "$NONE" ]; then
|
|
if [ -f "$MANAGED" ]; then cp -p "$MANAGED" "$BACKUP"; else touch "$NONE"; fi
|
|
fi
|
|
|
|
# 2. Merge only the two keys under test; everything else (the member hooks) stays.
|
|
"$NODE" -e '
|
|
const fs = require("fs"), [file, frag] = process.argv.slice(1);
|
|
let d = {};
|
|
try { d = JSON.parse(fs.readFileSync(file, "utf8")); } catch (e) { if (e.code !== "ENOENT") throw e; }
|
|
const f = JSON.parse(fs.readFileSync(frag, "utf8"));
|
|
for (const k of ["extraKnownMarketplaces", "enabledPlugins"]) if (f[k]) d[k] = { ...(d[k] || {}), ...f[k] };
|
|
fs.writeFileSync(file, JSON.stringify(d, null, 2) + "\n", { mode: 0o644 });
|
|
' "$MANAGED" "$FRAG"
|
|
cp "$MANAGED" "$OUT/managed-settings.applied.json"
|
|
|
|
# 3. A fresh account, first run seeded like agenthub-provision does.
|
|
useradd --create-home --shell /bin/bash --comment "AgentHub P6 spike (delete me)" "$U"
|
|
as_user "$NODE" -e '
|
|
const fs = require("fs"), home = process.env.HOME, p = home + "/.claude.json";
|
|
fs.writeFileSync(p, JSON.stringify({ hasCompletedOnboarding: true,
|
|
projects: { [home]: { hasTrustDialogAccepted: true } } }, null, 2) + "\n", { mode: 0o600 });'
|
|
as_user "$CLAUDE" --version >"$OUT/claude-version.txt" 2>&1 || true
|
|
|
|
# 4. Before any session: does the CLI alone see them?
|
|
as_user "$CLAUDE" plugin marketplace list --json >"$OUT/marketplaces.before.json" 2>&1 || true
|
|
as_user "$CLAUDE" plugin list --json >"$OUT/plugins.before.json" 2>&1 || true
|
|
|
|
# 5. One interactive start (managed marketplaces may sync only there). A
|
|
# private socket; kill-session only — never kill-server.
|
|
as_user tmux -L agh-spike new-session -d -s spike -x 200 -y 50 "$CLAUDE"
|
|
sleep "$SECS"
|
|
as_user tmux -L agh-spike capture-pane -p -t spike >"$OUT/session-screen.txt" 2>&1 || true
|
|
as_user tmux -L agh-spike kill-session -t spike 2>/dev/null || true
|
|
|
|
# 6. After.
|
|
as_user "$CLAUDE" plugin marketplace list --json >"$OUT/marketplaces.after.json" 2>&1 || true
|
|
as_user "$CLAUDE" plugin list --json >"$OUT/plugins.after.json" 2>&1 || true
|
|
find "$(getent passwd "$U" | cut -d: -f6)/.claude" -maxdepth 3 >"$OUT/claude-dir.txt" 2>&1 || true
|
|
|
|
# 7. Verdict per plugin the fragment enables.
|
|
"$NODE" -e '
|
|
const fs = require("fs"), [frag, out] = process.argv.slice(1);
|
|
const want = Object.keys(JSON.parse(fs.readFileSync(frag, "utf8")).enabledPlugins || {});
|
|
const read = (f) => fs.readFileSync(`${out}/${f}`, "utf8");
|
|
for (const p of want) {
|
|
const at = (f) => (read(f).includes(p) ? "listed" : "absent");
|
|
console.log(`${p}: before session ${at("plugins.before.json")}, after ${at("plugins.after.json")}`);
|
|
}
|
|
' "$FRAG" "$OUT" | tee "$OUT/verdict.txt"
|
|
echo "spike: details in $OUT; the screen it showed: $OUT/session-screen.txt"
|
|
echo "spike: now run '$0 undo'"
|
|
;;
|
|
undo)
|
|
if [ -e "$BACKUP" ]; then
|
|
mv -f "$BACKUP" "$MANAGED"
|
|
elif [ -e "$NONE" ]; then
|
|
rm -f "$MANAGED" "$NONE"
|
|
else
|
|
echo "spike: no backup — managed-settings.json left as it is"
|
|
fi
|
|
if id "$U" >/dev/null 2>&1; then
|
|
pkill -KILL -u "$U" 2>/dev/null || true
|
|
userdel --remove "$U" 2>/dev/null || userdel "$U"
|
|
fi
|
|
# The install-system mark matches again only if the file is back byte for byte.
|
|
if [ -f /etc/agenthub/managed-settings.sha256 ]; then
|
|
sha256sum -c --status /etc/agenthub/managed-settings.sha256 &&
|
|
echo "spike: managed-settings.json matches install-system's mark again" ||
|
|
echo "spike: WARNING managed-settings.json differs from install-system's mark" >&2
|
|
fi
|
|
echo "spike: undone (the report in $OUT is kept)"
|
|
;;
|
|
*) die "usage: $0 run <fragment.json> [seconds] | undo" ;;
|
|
esac
|