feat(sandbox): vendor Anthropic document + frontend skills into image
Bakes docx, pdf, pptx, xlsx, and frontend-design skills from
github.com/anthropics/skills into /opt/claude-skills/ at image build time
(sparse-checkout, shallow clone). On each chat turn the pipe symlinks them
into $CLAUDE_CONFIG_DIR/skills/ via the new ensure_skills() method.
Symlinks (not copies) mean:
- zero per-chat disk cost regardless of user count;
- image rebuilds that add or update a skill propagate automatically to
existing chats on their next turn (ln -sfn replaces stale targets);
- skills are read-only from the user's perspective — any attempt to
mutate them hits /opt/claude-skills, which is root-owned.
The skill list is surfaced as a valve (SKILLS) so operators can disable
individual skills without a rebuild; it must remain a subset of what the
Dockerfile CLAUDE_SKILLS arg pulled in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6af5180513
commit
5c41f57f0b
|
|
@ -269,6 +269,30 @@ class OpenTerminalClient:
|
|||
async for _ in self.stream_output(handle):
|
||||
pass
|
||||
|
||||
async def ensure_skills(
|
||||
self, user_id: str, workdir: str, skills: List[str]
|
||||
) -> None:
|
||||
"""Symlink system-vendored skills from /opt/claude-skills/ into the
|
||||
chat's CLAUDE_CONFIG_DIR so Claude picks them up at startup. Uses
|
||||
`ln -sfn` so image rebuilds that change the skill set propagate to
|
||||
existing chats on their next turn (the old symlink is replaced, not
|
||||
skipped). No-op if the skills list is empty."""
|
||||
if not skills:
|
||||
return
|
||||
for s in skills:
|
||||
# Skill names are baked into the image path; reject anything
|
||||
# that could break out of the /opt/claude-skills/ prefix.
|
||||
if not re.match(r"^[a-zA-Z0-9_-]+$", s):
|
||||
raise OpenTerminalError(f"unsafe skill name: {s!r}")
|
||||
parts = [f"mkdir -p {workdir}/.claude/skills"]
|
||||
parts += [
|
||||
f"ln -sfn /opt/claude-skills/{s} {workdir}/.claude/skills/{s}"
|
||||
for s in skills
|
||||
]
|
||||
handle = await self.start(user_id, " && ".join(parts))
|
||||
async for _ in self.stream_output(handle):
|
||||
pass
|
||||
|
||||
async def run_capture(self, user_id: str, command: str) -> str:
|
||||
"""Run a short command and return its merged stdout/stderr as a single
|
||||
string. For one-shot utility commands (ls, cat of tiny files) where
|
||||
|
|
@ -715,6 +739,16 @@ class Pipe:
|
|||
"{chat_id} is substituted. Artifacts persist across turns."
|
||||
),
|
||||
)
|
||||
SKILLS: str = Field(
|
||||
default="docx,pdf,pptx,xlsx,frontend-design",
|
||||
description=(
|
||||
"Comma-separated list of skill names to symlink into each "
|
||||
"chat's .claude/skills/ from /opt/claude-skills/ in the "
|
||||
"sandbox image. Must match directories baked into the image "
|
||||
"at build time (see sandbox/Dockerfile CLAUDE_SKILLS arg). "
|
||||
"Empty string disables."
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.valves = self.Valves()
|
||||
|
|
@ -810,6 +844,13 @@ class Pipe:
|
|||
) as client:
|
||||
await client.ensure_dir(sandbox_user, workdir)
|
||||
|
||||
skills = [
|
||||
s.strip()
|
||||
for s in (self.valves.SKILLS or "").split(",")
|
||||
if s.strip()
|
||||
]
|
||||
await client.ensure_skills(sandbox_user, workdir, skills)
|
||||
|
||||
# Cache miss = either a brand-new chat OR an OWUI restart
|
||||
# dropped the in-process map (#5). Claude's own on-disk
|
||||
# session JSONL tells us which — look there before giving
|
||||
|
|
|
|||
|
|
@ -17,11 +17,32 @@ RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|
|||
# bubblewrap + socat are the Linux backends for Claude Code's built-in
|
||||
# `/sandbox` feature. Without them, Claude falls back to unsandboxed bash —
|
||||
# our managed-settings.json sets failIfUnavailable=true so that silent
|
||||
# degradation becomes a hard error instead.
|
||||
# degradation becomes a hard error instead. git is needed for the skill
|
||||
# vendoring step below.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends bubblewrap socat \
|
||||
&& apt-get install -y --no-install-recommends bubblewrap socat git ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Vendor a curated set of Anthropic skills into /opt/claude-skills. Each
|
||||
# chat's CLAUDE_CONFIG_DIR/.claude/skills/ symlinks here at runtime, so all
|
||||
# users share a read-only copy without per-chat duplication. To update the
|
||||
# set, change the sparse-checkout list and rebuild — running chats pick up
|
||||
# the new set on their next turn (symlinks re-point).
|
||||
ARG CLAUDE_SKILLS="docx pdf pptx xlsx frontend-design"
|
||||
RUN set -eux; \
|
||||
mkdir -p /opt/claude-skills; \
|
||||
tmp="$(mktemp -d)"; \
|
||||
git clone --depth 1 --filter=blob:none --sparse \
|
||||
https://github.com/anthropics/skills.git "$tmp"; \
|
||||
cd "$tmp"; \
|
||||
paths=""; for s in $CLAUDE_SKILLS; do paths="$paths skills/$s"; done; \
|
||||
git sparse-checkout set $paths; \
|
||||
for s in $CLAUDE_SKILLS; do \
|
||||
cp -r "skills/$s" "/opt/claude-skills/$s"; \
|
||||
done; \
|
||||
cd /; rm -rf "$tmp"; \
|
||||
ls /opt/claude-skills/
|
||||
|
||||
# Managed (enterprise) settings live at the system-wide path and take
|
||||
# precedence over per-user and per-project settings. This is how we pin
|
||||
# Bash egress + WebFetch allowlists: users can extend via their own
|
||||
|
|
|
|||
Loading…
Reference in New Issue