mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
D2: seed baked LSP plugins into persistent state on first boot
The image installs the LSP plugins under /root/.claude, but the launcher symlinks /root/.claude -> /agent-vm-state/claude for persistence, which shadows the baked tree — the booted guest's `claude plugin list` was empty. Stash the plugins + settings to /opt/agent-vm/claude-seed (not shadowed) and ship seed-claude-plugins.sh; the launch prelude runs it before exec'ing the agent, copying the stash into the state dir once and merging enabledPlugins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2de958638
commit
ef76b22733
3 changed files with 70 additions and 0 deletions
|
|
@ -1835,6 +1835,16 @@ fn parse_github_slug(url: &str) -> Option<String> {
|
|||
const STRIP_IPV6_NAMESERVERS: &str =
|
||||
"sed -i '/^nameserver .*:/d' /etc/resolv.conf 2>/dev/null || true";
|
||||
|
||||
/// Seed the image's baked Claude LSP plugins into the persistent state dir on
|
||||
/// first boot (PLAN.md D2). The image installs them under `/root/.claude`, but
|
||||
/// the persistence symlink (`/root/.claude -> /agent-vm-state/claude`) shadows
|
||||
/// that tree so the booted guest's `claude plugin list` is empty. The image
|
||||
/// ships `/opt/agent-vm/seed-claude-plugins.sh`, which copies the stash into
|
||||
/// the state dir once. Guarded on the script's presence so older images (no
|
||||
/// stash) are an inert no-op, and idempotent so it only does work on first boot.
|
||||
const SEED_CLAUDE_PLUGINS: &str =
|
||||
"[ -x /opt/agent-vm/seed-claude-plugins.sh ] && /opt/agent-vm/seed-claude-plugins.sh || true";
|
||||
|
||||
/// Build the `bash -c` line that runs inside the guest: the prelude
|
||||
/// (IPv6-nameserver strip, stdin redirect, optional chrome-CA install,
|
||||
/// optional project runtime hook) followed by `exec`'ing the chosen
|
||||
|
|
@ -1850,6 +1860,7 @@ fn build_agent_shell_line(
|
|||
let path = shell_escape(project_guest_path);
|
||||
let prelude = format!(
|
||||
"{STRIP_IPV6_NAMESERVERS}\n\
|
||||
{SEED_CLAUDE_PLUGINS}\n\
|
||||
[ -t 0 ] || exec < /dev/null\n\
|
||||
{chrome_mcp_prelude}\
|
||||
_hook={path}/.agent-vm.runtime.sh\n\
|
||||
|
|
@ -1919,6 +1930,16 @@ mod tests {
|
|||
assert!(line.contains("exec 'codex' 'exec'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_agent_shell_line_seeds_claude_plugins_before_exec() {
|
||||
// D2: the prelude seeds the baked LSP plugins into the persistent
|
||||
// state dir, and must do so before the agent execs.
|
||||
let line = build_agent_shell_line("/work/proj", "", "claude", &[]);
|
||||
let seed = line.find(SEED_CLAUDE_PLUGINS).expect("seed step present");
|
||||
let exec = line.find("exec 'claude'").expect("exec present");
|
||||
assert!(seed < exec, "seed must run before exec; got: {line}");
|
||||
}
|
||||
|
||||
/// Guard the exact `sed` program in `STRIP_IPV6_NAMESERVERS`. The
|
||||
/// regex is load-bearing (see the const's doc comment): an accidental
|
||||
/// edit that dropped the `^` anchor or the `:` class would silently
|
||||
|
|
|
|||
|
|
@ -303,6 +303,17 @@ RUN { /root/.local/bin/claude plugin marketplace add anthropics/claude-plugins-o
|
|||
&& echo "==> LSP plugins installed at build time:" \
|
||||
&& { /root/.local/bin/claude plugin list || true; }
|
||||
|
||||
# D2 fix: the running guest symlinks /root/.claude -> /agent-vm-state/claude
|
||||
# (persistence, run.rs), which shadows the plugin tree baked just above so the
|
||||
# booted guest's `claude plugin list` shows nothing. Stash the plugins +
|
||||
# settings to a NON-shadowed path and ship a first-boot seed script; the
|
||||
# launcher prelude calls it before exec'ing the agent.
|
||||
RUN mkdir -p /opt/agent-vm/claude-seed \
|
||||
&& cp -a /root/.claude/plugins /opt/agent-vm/claude-seed/plugins \
|
||||
&& cp -a /root/.claude/settings.json /opt/agent-vm/claude-seed/settings.json
|
||||
COPY seed-claude-plugins.sh /opt/agent-vm/seed-claude-plugins.sh
|
||||
RUN chmod +x /opt/agent-vm/seed-claude-plugins.sh
|
||||
|
||||
# OpenCode official installer. Installs into ~/.opencode/bin; PATH already
|
||||
# includes it. Symlink into /usr/local/bin so non-login shells find it too.
|
||||
RUN curl -fsSL https://opencode.ai/install | bash \
|
||||
|
|
|
|||
38
images/seed-claude-plugins.sh
Normal file
38
images/seed-claude-plugins.sh
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#!/bin/sh
|
||||
# Seed the image's baked Claude LSP plugins into the persistent state dir on
|
||||
# first boot.
|
||||
#
|
||||
# The image installs the plugins into /root/.claude/plugins at build time, but
|
||||
# the launcher symlinks /root/.claude -> /agent-vm-state/claude for persistence
|
||||
# (run.rs), which shadows the baked tree so `claude plugin list` sees nothing.
|
||||
# This script (called from the launcher prelude before the agent execs) copies
|
||||
# the stash into the state dir once and merges the enabledPlugins +
|
||||
# extraKnownMarketplaces keys into the state settings.json, preserving anything
|
||||
# the user already set. Best-effort: never fails the launch.
|
||||
#
|
||||
# Idempotent: a present /agent-vm-state/claude/plugins means we already seeded
|
||||
# (or the user manages plugins themselves) — do nothing.
|
||||
SEED=/opt/agent-vm/claude-seed
|
||||
STATE=/agent-vm-state/claude
|
||||
|
||||
[ -d "$SEED/plugins" ] || exit 0
|
||||
[ -e "$STATE/plugins" ] && exit 0
|
||||
|
||||
mkdir -p "$STATE" 2>/dev/null
|
||||
cp -a "$SEED/plugins" "$STATE/plugins" 2>/dev/null || exit 0
|
||||
|
||||
if [ -f "$SEED/settings.json" ] && command -v node >/dev/null 2>&1; then
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
// With `node -e CODE a b`, user args start at argv[1] (no script path slot).
|
||||
const [, statePath, seedPath] = process.argv;
|
||||
const seed = JSON.parse(fs.readFileSync(seedPath, "utf8"));
|
||||
let st = {};
|
||||
try { st = JSON.parse(fs.readFileSync(statePath, "utf8")); } catch (e) {}
|
||||
st.enabledPlugins = Object.assign({}, seed.enabledPlugins || {}, st.enabledPlugins || {});
|
||||
st.extraKnownMarketplaces = Object.assign({}, seed.extraKnownMarketplaces || {}, st.extraKnownMarketplaces || {});
|
||||
fs.writeFileSync(statePath, JSON.stringify(st, null, 2));
|
||||
' "$STATE/settings.json" "$SEED/settings.json" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue