Commit Graph

25 Commits

Author SHA1 Message Date
Speedliner 72e84da09b
Merge bf2c8a28c6 into 5bbc1fcdf9 2026-08-13 00:53:17 -03:00
Assistant Patch bf2c8a28c6 Merge fix/inline-session-modules into main: resolve ModuleNotFoundError by inlining session_store/session_marker 2026-08-10 20:53:35 +00:00
Assistant Patch 86933746aa Fix: inline session_store/session_marker into claude_agent_pipe.py
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.
2026-08-10 20:45:48 +00:00
Speedliner 3b4fa5e099
Merge pull request #1 from Speedliner/fix/persistent-session-tracking
Fix: persist Claude Code session id across process restarts and workers
2026-08-10 15:33:41 +02:00
Assistant Patch 3c2217115b Fix: persist Claude Code session id across process restarts and workers
- 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).
2026-08-10 13:21:36 +00:00
Claudius Magicus 8f0d99100b chore: strip orphaned whitespace-only line in rate-limit event handler
Cosmetic cleanup of a leftover whitespace line after the rate_limit_event
branch in _handle_event (from 6ac8d3b), no functional change.
2026-08-07 16:09:12 +02:00
Claudius Magicus 7e48b82b22 fix: repair IndentationError in claude_agent_pipe.py import block
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.
2026-08-07 16:02:34 +02:00
Speedliner 3b3092dc75
Update claude_agent_pipe_sandboxed.py 2026-08-07 15:45:22 +02:00
Speedliner 4387685614
Update claude_agent_pipe.py 2026-08-07 15:45:09 +02:00
Speedliner 6ac8d3b52c
Update claude_agent_pipe_sandboxed.py 2026-08-07 15:44:42 +02:00
Speedliner ee6fc4c3c5
Update claude_agent_pipe.py 2026-08-07 15:42:09 +02:00
Speedliner ae4955834d
Update README.md
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.
2026-07-23 17:56:56 +02:00
Speedliner 1cb590db23
Update claude_agent_pipe.py
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)
2026-07-23 17:44:59 +02:00
Thomas Friedel 5bbc1fcdf9 feat(pipe): add SETTING_SOURCES valve to opt into CLAUDE.md loading
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>
2026-06-01 21:32:10 +02:00
Thomas Friedel e100c801c4 fix(sandbox): expand ~ in CLAUDE_CONFIG_DIR + prune .claude from artifacts
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>
2026-04-19 00:02:04 +02:00
Thomas Friedel 05d53ade55 feat(sandbox): install document + DS tooling for vendored skills
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>
2026-04-18 23:49:08 +02:00
Thomas Friedel 5c41f57f0b 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>
2026-04-18 23:44:20 +02:00
Thomas Friedel 6af5180513 feat(sandbox): extend managed allowlist with build + dataset egress domains
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>
2026-04-18 23:39:34 +02:00
Thomas Friedel c5f6764470 feat(sandbox): enable Claude Code built-in sandbox for Bash + WebFetch egress
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>
2026-04-18 23:31:07 +02:00
Thomas Friedel af2e69a4bb feat(sandbox): recover chat session mapping after OWUI restart
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>
2026-04-18 23:04:25 +02:00
Thomas Friedel 3cec4dc871 feat(sandbox): pin CLI, reap orphans, isolate chats, cleanup script
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>
2026-04-18 22:20:29 +02:00
Thomas Friedel 4158af9ebe feat(sandbox): route Anthropic traffic through credential-injecting proxy
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>
2026-04-18 21:59:59 +02:00
Thomas Friedel 7e7fa4cb77 feat: sandboxed Claude Code pipe via open-terminal
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>
2026-04-18 21:02:01 +02:00
Thomas Friedel a4898f6d9a Disable fast path — always run full agent loop 2026-04-18 15:19:57 +02:00
Thomas Friedel 1544ec1944 Initial commit: Claude Code pipe for Open WebUI 2026-04-18 14:55:03 +02:00