Commit Graph

12 Commits

Author SHA1 Message Date
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