Skip to content

Commit d9f036b

Browse files
CodeCasterXclaude
andcommitted
feat(docker): 沙箱预植入 Claude Code 插件缓存并安装 pyright LSP
沙箱创建时将宿主机的 ~/.claude/plugins/ 目录(含 cache、marketplaces、 installed_plugins.json)递归复制到沙箱,并将绝对路径改写为容器路径, 解决容器内 Claude Code 启动时所有插件 "failed to load" 的问题。 同时在 Dockerfile 中安装 pyright 语言服务器,解决 pyright-lsp 插件 注册的 MCP 服务器因 pyright-langserver 缺失而启动失败的问题。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ade9201 commit d9f036b

3 files changed

Lines changed: 39 additions & 0 deletions

File tree

docker/sandbox/Dockerfile.runtime-only

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ RUN if [ -z "${AI_TOOL_PACKAGES}" ]; then \
6060
fi && \
6161
npm install -g ${AI_TOOL_PACKAGES}
6262

63+
# LSP 语言服务器(Claude Code 的 pyright-lsp / jdtls-lsp 插件依赖)
64+
RUN npm install -g pyright
65+
6366
# 预创建 .local 目录结构(避免 Docker 卷挂载以 root 创建中间目录导致权限问题)
6467
RUN mkdir -p /home/devuser/.local/share /home/devuser/.local/state
6568

docker/sandbox/src/commands/create.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,28 @@ export async function create(branch: string, base: string | undefined, opts: Cre
143143
fs.copyFileSync(hostPath, dest);
144144
}
145145
}
146+
// Recursively copy host directories into sandbox
147+
for (const { hostDir, sandboxSubdir } of tool.hostPreSeedDirs ?? []) {
148+
const dest = path.join(dir, sandboxSubdir);
149+
if (fs.existsSync(hostDir) && !fs.existsSync(dest)) {
150+
fs.cpSync(hostDir, dest, { recursive: true });
151+
}
152+
}
153+
// Rewrite host paths to container paths in specified files
154+
if (tool.pathRewriteFiles?.length) {
155+
const hostHome = process.env.HOME!;
156+
const containerHome = path.dirname(tool.containerMount);
157+
for (const relFile of tool.pathRewriteFiles) {
158+
const filePath = path.join(dir, relFile);
159+
if (fs.existsSync(filePath)) {
160+
let content = fs.readFileSync(filePath, 'utf8');
161+
// Replace project path first (more specific), then home dir
162+
content = content.replaceAll(MAIN_REPO, '/workspace');
163+
content = content.replaceAll(hostHome, containerHome);
164+
fs.writeFileSync(filePath, content);
165+
}
166+
}
167+
}
146168
}
147169

148170
// Build env args and volume mounts from tool registry

docker/sandbox/src/tools.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ export interface AiTool {
3232
postSetupCmds?: string[];
3333
/** Extra environment variables to pass to the container */
3434
envVars?: Record<string, string>;
35+
/** Host directories to recursively copy into sandbox (skipped if target already exists) */
36+
hostPreSeedDirs?: Array<{ hostDir: string; sandboxSubdir: string }>;
37+
/**
38+
* Files (relative to sandbox dir) that need host→container path rewriting after copy.
39+
* Rewrites: HOST_HOME → container home, HOST_PROJECT → /workspace.
40+
*/
41+
pathRewriteFiles?: string[];
3542
}
3643

3744
/** Validate descriptor consistency at startup — fail fast on misconfiguration. */
@@ -68,6 +75,13 @@ export const AI_TOOLS: readonly Readonly<AiTool>[] = [
6875
versionCmd: 'claude --version',
6976
noAuthHint: '首次使用需在容器内运行 claude 完成一次 OAuth 登录,之后免登录。',
7077
envVars: { CLAUDE_CONFIG_DIR: '/home/devuser/.claude' },
78+
hostPreSeedDirs: [
79+
{ hostDir: path.join(HOME, '.claude', 'plugins'), sandboxSubdir: 'plugins' },
80+
],
81+
pathRewriteFiles: [
82+
'plugins/installed_plugins.json',
83+
'plugins/known_marketplaces.json',
84+
],
7185
},
7286
{
7387
name: 'Codex',

0 commit comments

Comments
 (0)