OpenWebUI loads Functions as a single in-memory module via exec()
(open_webui/utils/plugin.py: load_function_module_by_id). There is no
mechanism to ship sibling .py files alongside a Function, so
'from session_store import get_store' / 'from session_marker import ...'
fail with ModuleNotFoundError as soon as the function is pasted/imported
into OpenWebUI (see traceback: exec(content, module.__dict__) -> line 24
-> ModuleNotFoundError: No module named 'session_store').
Fix: inline both modules' full content directly into
claude_agent_pipe.py so it is fully self-contained, and remove the now
redundant standalone session_store.py / session_marker.py files (their
logic lives inline now; keeping both would only invite drift).
Verified by:
- python3 -m py_compile claude_agent_pipe.py
- exec()'ing the file the same way OpenWebUI's plugin loader does (with
claude_agent_sdk stubbed out), confirming no ModuleNotFoundError/NameError
and that SessionStore/get_store/make_marker/extract_session_id_from_messages
and the Pipe class all load correctly.
- Add session_store.py: SQLite-backed, WAL-mode session store keyed by
chat_id, robust to multi-worker access and process restarts.
- Add session_marker.py: fallback mechanism that embeds an invisible
markdown reference marker in assistant responses so the session id
survives even without SQLite access, by parsing it back out of
body messages history.
- Update claude_agent_pipe.py: resolution order is in-memory cache ->
SQLite store -> marker fallback. Session id is persisted to both the
in-memory dict and SQLite on every init SystemMessage, and the
marker is appended to the final visible response text.
- Warn when chat_id is missing/None instead of silently starting
a fresh session (related to open-webui/open-webui#20563).
The rate-limit-surfacing feature (RateLimitEvent handling) added in
ee6fc4c/4387685 left a stray leading space on the claude_agent_sdk
import block, causing a module-level IndentationError that broke the
whole pipe. Also cleans up two orphaned whitespace-only lines left
between the RateLimitEvent and ResultMessage branches.
docs: document setup-token scope bug and Docker credential persistence
`claude setup-token` has been reported to sometimes issue OAuth tokens
scoped too narrowly for API use (anthropics/claude-code#23703),
resulting in 401 "Invalid bearer token" on every pipe request despite
a seemingly valid token. Ran into this directly; switching to a full
interactive `claude` login resolved it immediately.
That in turn surfaces an undocumented Docker gotcha: interactive-login
credentials are written to ~/.claude and ~/.claude.json inside the
container, which are lost on container recreation unless explicitly
volume-mounted (the CLAUDE_CODE_OAUTH_TOKEN Valve itself is fine, since
Valves persist in OpenWebUI's DB — it's the filesystem-based login that
isn't). Added a Docker / persistence notes section with the volume
config and one-time login command needed to make this durable.
Add OpenWebUI Tools/MCP passthrough, fix multi-arity bug, bump to 0.2
- Fix _build_kb_mcp_server: the no-knowledge branch returned a 2-tuple
(None, []) while every call site unpacked 3 values, raising
ValueError: not enough values to unpack (expected 3, got 2) on any
turn without an attached knowledge base. Now consistently returns
(None, [], {}).
- Add __tools__ passthrough: wrap OpenWebUI's Tools / external tool
servers (incl. MCP via mcpo) attached to the Workspace Model as an
in-process MCP server ("owui-tools"), merged alongside the existing
"helm-kb" server. Lets Claude Code use whatever tools/connectors are
configured in OpenWebUI natively, without hardcoding a server URL in
the pipe — stays in sync if the attached tools change later.
New: _JSON_SCHEMA_TYPE_MAP, _build_owui_tools_mcp_server().
Known limitation: OpenWebUI's built-in tools (web_search,
image_generation, execute_code) aren't included in __tools__ yet
(upstream limitation); only user-defined Tools and external/MCP
tool servers are. Not an issue here since Claude Code already ships
its own WebSearch/WebFetch.
- version: 0.1 -> 0.2, contributors: Speedliner (author unchanged)
The SDK pipe hardcoded setting_sources=[], so chats never loaded
~/.claude/CLAUDE.md or any settings.json. Expose it as a valve
(default empty = isolated baseline) so single-user/homelab instances
can opt into persistent context, while shared deployments keep the
safe default.
Parse via _parse_setting_sources(): comma-split, lowercase, drop
unknown tokens so a typo can't silently widen inheritance. README
documents the opt-in plus the hooks/permissions security tradeoff
(CLAUDE.md and settings.json load together — a Claude Code coupling)
and why the valve does not apply to the sandboxed pipe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shlex.quote() wraps its argument in single quotes, which suppress tilde
expansion. CLAUDE_CONFIG_DIR='~/chat-<id>/.claude' was being handed to
Claude Code as a literal path beginning with `~`, so the CLI treated it
as a relative directory and created `./~/chat-<id>/.claude/` under cwd.
Concrete symptoms:
- Session history JSONLs written to the wrong path, silently defeating
#5 (disk-based session recovery couldn't find them).
- Per-chat config isolation (#9) silently degraded to a shared dir
whose actual location depended on whatever cwd the turn ran in.
- Symlinked skills placed at the correct path were never discovered by
Claude, because CLAUDE_CONFIG_DIR pointed elsewhere.
- `.claude.json` artifacts from the stray tree picked up by the
artifact walker → attempted upload → open-terminal 403 because the
`~` path segment failed its path-sanity check.
Fix by pre-expanding a leading `~/` to `$HOME/` and double-quoting so
$HOME expands at shell parse time. Also prune `.claude/` and any stray
`~` literal directory from the artifact find() so Claude's internal
state never masquerades as a user artifact going forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The docx/pdf/pptx/xlsx skills' scripts assume a working set of Python
libraries (pypdf, pdfplumber, reportlab, openpyxl, lxml, defusedxml,
Pillow, markitdown) and native tools (soffice headless, pdftoppm). Without
them, first-turn invocations of those skills fail with ImportError — and
the egress allowlist's pypi.org entry can't save us because pip install
on every turn adds latency and exercises an attack surface we'd rather
not hit on the critical path.
Bakes everything into the image:
- libreoffice-{core,writer,calc,impress}: drops the GUI/Java bloat of
the meta package while keeping headless conversion for all three
office formats. Adds ~500 MB vs ~800+ MB for the full meta.
- poppler-utils + fonts-dejavu: pdftoppm binary, pdf2image backend,
and baseline glyph set so rendered PDFs aren't empty boxes.
- pip: pypdf pdfplumber pdf2image reportlab Pillow openpyxl
python-docx python-pptx pandas numpy matplotlib lxml defusedxml
markitdown[pptx]. Also covers the explicit DS ask (pandas/numpy/
matplotlib) so ad-hoc analysis doesn't hit pip on first use.
- npm -g docx: the JS library the docx skill prefers for generating
richly formatted new documents.
Final image is ~4.6 GB (up from ~1 GB). LibreOffice is the dominant term.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Widens the Bash-subprocess egress allowlist for common agent workflows:
Anthropic direct (api, statsig), Ubuntu mirrors, NodeSource/deb for Node,
crates.io (Rust), yarn, full npm + pypi groups, GitHub release assets,
and wildcards for geoboundaries, googleapis, jsdelivr — covers dataset
fetches and CDN-backed library assets that previously failed silently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#10. Until now the sandbox container had unrestricted outbound NAT
on sandbox-net — a compromised agent could curl arbitrary hosts, pip/npm
install malicious packages, or exfiltrate files via WebFetch. The
anthropic-proxy path-allowlist (#6) guarded only the credential-injection
proxy, not general egress.
Two layers added, both shipped as managed (enterprise) settings so user
and project settings cannot downgrade them:
- sandbox.* → bubblewrap + local proxy gating Bash subprocesses (and all
their children: pip, npm, git, curl). enableWeakerNestedSandbox=true
because we run inside Docker without privileged user namespaces;
accepted because the Docker container boundary is unchanged and this
adds new egress filtering that previously did not exist.
- permissions.* → deny-by-default WebFetch with explicit domain allows
(WebFetch bypasses the OS sandbox and needs its own allowlist).
failIfUnavailable=true + allowUnsandboxedCommands=false close the two
silent-fallback paths: if bubblewrap/socat install breaks, the container
fails loud instead of serving unsandboxed bash; and the
dangerouslyDisableSandbox escape hatch is disabled so sandbox violations
can't be bypassed via a permission prompt in our unattended deployment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#5. In-process _chat_sessions dict is empty after any restart, which
silently started a fresh Claude session on the next turn and lost the prior
conversation. On cache miss we now scan the per-chat CLAUDE_CONFIG_DIR on
disk, pick the newest session JSONL by mtime, and resume from it — Claude's
own filesystem layout is already the source of truth. UUID regex guards
against unexpected files in that directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses four hardening items in one change:
- #8 Pin @anthropic-ai/claude-code to 2.1.114 via Dockerfile ARG.
`docker compose build --build-arg CLAUDE_CODE_VERSION=X.Y.Z` upgrades.
- #2 Move `client.kill()` from an `except CancelledError` branch into a
`finally` guarded by an `exited_cleanly` flag. Any abnormal exit path
(httpx timeout, generator GC'd, unexpected exception) now reaps the
open-terminal child — previously only asyncio cancellation did, so
dropped browsers left claude billing tokens until EXECUTE_TIMEOUT.
- #9 Per-chat CLAUDE_CONFIG_DIR=~/chat-<id>/.claude. Two concurrent
chats for the same OWUI user no longer race each other on a shared
~/.claude/. Safe to shard now that credentials live in the proxy
rather than .credentials.json.
- #1 sandbox/cleanup.sh installed at /opt/cleanup.sh. Not auto-run;
README documents dry-run-by-default usage via `docker compose exec`.
CHAT_TTL_DAYS and SESSION_TTL_DAYS control retention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Caddy-based reverse proxy (`anthropic-proxy/`) that forwards
/v1/* to api.anthropic.com with the real credential injected at the
proxy, not in the sandbox. The sandbox now only sees
ANTHROPIC_BASE_URL=http://anthropic-proxy:8081 and a placeholder
ANTHROPIC_AUTH_TOKEN, so `env` / `cat /proc/self/environ` yield nothing.
Proxy auth mode:
- CLAUDE_CODE_OAUTH_TOKEN → `Authorization: Bearer` + appends
`oauth-2025-04-20` to anthropic-beta (required for subscription OAuth
on /v1/messages).
- ANTHROPIC_API_KEY → `x-api-key`.
Pipe changes:
- Drop ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN valves.
- Add ANTHROPIC_BASE_URL valve (defaults to the compose service DNS).
- Replace credential env injection with ANTHROPIC_AUTH_TOKEN=proxied.
Closes acceptance criteria for issue #6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `claude_agent_pipe_sandboxed.py`, a self-contained OpenWebUI pipe
that shells `claude --output-format stream-json` inside an open-webui/
open-terminal container instead of running the Claude Agent SDK
in-process. Each OWUI user gets a dedicated Linux account (mapped via
the X-User-Id header) with a per-chat workspace directory that persists
across turns and whose generated PNG/PDF/CSV artifacts are auto-inlined
into the chat.
Scaffolding under `sandbox/`:
- Dockerfile extending ghcr.io/open-webui/open-terminal with Claude
Code pre-installed system-wide (inherited by every provisioned user)
- docker-compose.yml + README covering standalone deployment
- Async HTTP client + stream-json runner for programmatic use
`sync_pipe.py` pushes the pipe to a running OWUI via the admin
functions API so iteration doesn't require manual repaste.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>