Self-host: it-ops broker, per-user auth, preview isolation, visual editor + comments#2
Open
holoduke wants to merge 162 commits into
Open
Conversation
- git-provider.ts: env-driven provider config (github|gitea) - github.ts: provider-aware repo create/connect/push (Gitea /api/v1, org repos) - scaffold-deploy.ts: inject Dockerfile + compose + .gitea workflow per site - Dockerfile/compose/.gitea: containerize Claudable with claude CLI (claude -p)
- claude-opus-4-8 as the top/flagship Opus option - generic 'opus' aliases now resolve to 4.8; 4.6 kept as previous-gen
- preview.ts: PREVIEW_URL_TEMPLATE for proxy-routed preview URLs, PREVIEW_BIND_HOST to bind 0.0.0.0 - compose: preview band 3710-3719, preview.newstory.tf template, bind 0.0.0.0
- Dockerfile: run as node user (Claude Code refuses skip-permissions as root) - claude.ts: pass cwd so agent edits the project, not Claudable's own /app - preview.ts: force NODE_ENV=development so next dev compiles CSS (was 500)
- claude.ts: settingSources ['project','user'] so the agent loads .claude/skills/ - lib/services/skills.ts: read/write/delete SKILL.md under <project>/.claude/skills/<name>/ - API: GET/POST /api/projects/[id]/skills, GET/DELETE /api/projects/[id]/skills/[name] - UI: SkillsSettings panel + new Skills tab in ProjectSettings
Self-hosted Gitea deploy, Opus 4.8, server-hosting fixes, per-project Skills
- scaffold.ts: Nuxt 4 + @nuxt/ui scaffold (app.vue/UApp, pages/, nuxt dev wrapper, local postcss) - claude.ts: system prompt + initial prompt target Nuxt + Nuxt UI skill; default Opus 4.8 - claudeModels.ts: CLAUDE_DEFAULT_MODEL = claude-opus-4-8 - scaffold-deploy.ts: Nuxt Dockerfile (node .output/server) + nitro host/port env + dockerignore
…view instructions)
… in deploy.sh - Dockerfile: COPY --chown + USER node early; no 'chown -R /app' pass - deploy.sh (box): flock so poller and manual deploys can't race
…onfig.js (unsupported by Nuxt)
…he parent (broke agent session-env)
…s) so remote previews work
…op fake loading delay)
…keep auto-recover silent
…g per message, dropping live updates
…rver), keep SSE always-on (stops connect/disconnect thrash)
…t (only reconnect on CLOSED)
A request is driven entirely in-process (running -> completed/failed in a finally block). If the process is killed mid-run (container redeploy/OOM), that finally never executes and the row is stuck in an active status. This made hasActiveRequests permanently true, suppressing preview auto-start and showing a perpetual processing state. - instrumentation.ts: on server boot, reconcile any active-status request to failed (no request can legitimately be running right after a fresh start). - getActiveRequests: ignore rows older than 20m as a runtime backstop for runs that die without a restart.
…ips instrumentation) next start with output:standalone does not execute the instrumentation register() hook, so move the once-per-process crash recovery into the prisma client module — the one place that always loads on the server regardless of launch mode. Guarded by a global flag so it runs exactly once.
- preview: serialize concurrent start() per project (Map of in-flight promises) so two callers can't spawn duplicate dev servers and exhaust the port range. - preview: always clear the 'starting' state after the readiness wait so a silent dev server no longer leaves the UI spinning forever; throw if the process exited before becoming reachable instead of marking it running. - preview: drop dead process entries in the child 'error' handler so a later start() isn't blocked by a stale map entry. - act route: the fire-and-forget executor .catch() now marks the request failed, closing another path that could leak a stuck 'processing' row.
- system prompt: explicit guidance for visual design quality, light/dark theming, mobile-first responsiveness, WCAG AA accessibility, per-page useSeoMeta, SSR-friendly data fetching, and using available skills. - scaffold: add @nuxt/image and @nuxt/fonts modules; add app.config.ts theme baseline (brand colors); add SEO/head defaults (lang, title template, viewport, description) in nuxt.config; replace the bare placeholder index page with a real landing page (hero + feature grid) that calls useSeoMeta.
'latest' is unpinned: new apps would silently adopt a breaking major (Nuxt 5, Nuxt UI 5) the day it ships and could fail to build. Pin to the current majors (nuxt ^4.4.8, @nuxt/ui ^4.9.0, @nuxt/image ^2.0.0, @nuxt/fonts ^0.14.0) so apps still get all minor/patch updates but major bumps are deliberate.
Thinking mode was a dead boolean prop (no UI, never sent to the backend, ignored by claude.ts). Wire it end-to-end: - ChatInput: a 'Thinking' selector (Auto / Deep / Off), shown for Claude. - page.tsx + /act route: send thinkingMode with each request. - claude.ts: map to the Agent SDK thinking config (auto=adaptive, forced=enabled 31999 budget, off=disabled) and surface thinking content blocks wrapped in <thinking> so ChatLog renders them as collapsible reasoning sections.
A fixed budgetTokens (31999) exceeded maxOutputTokens (4000); the API rejects budget >= max_tokens, which broke runs with forced thinking. Switch to adaptive thinking with effort:'high' for 'forced' (deep reasoning) and plain adaptive for 'auto' — no budget/max constraint, and the SDK's recommended control on modern models.
getMessagesByProjectId ordered createdAt asc with skip/take, so the initial offset=0 load returned the OLDEST messages. With a long history the user's most recent messages were never loaded (looked like they weren't saved). Order desc and reverse so offset=0 returns the latest batch; load-older paginates back.
Typing '/' surfaces the project + global skills (name + description + scope), with ↑↓ navigation, Enter/Tab to insert, Esc to dismiss. Selecting inserts '/skill ' — which the Agent SDK dispatches as a real slash-command invocation (verified: our skills appear in the SDK slash_commands list). Fetches from /api/projects/[id]/skills; only shows enabled skills.
…mposer) Anchor the menu with fixed coords to the input and open it to the RIGHT (over the preview) where there's room — falls back to above the input on narrow layouts. Escapes the composer card's clipping; z-[200]. Reposition on scroll/resize.
…ring below the viewport)
Redesign /login: cinematic dark hero with animated warm gradient blobs, a huge Gill Sans Nova wordmark (gradient text), Baystar Script accent, staggered fade-up entrance, a shimmer-sweep Google button, and a scrolling marquee strip. Adds the Salsa Shop fonts (Gill Sans Nova, Founders Grotesk, Baystar Script) to public/fonts + @font-face in globals.css. AppShell renders /login bare (no app header).
Adds a static import kind: no scaffold, no npm install, served by a dependency-free node static file server (serves the repo, index.html default, SPA fallback). Guards scaffold/install/route-reporter paths, adds a static agent system prompt (edit the existing site in place, don't add a framework), and a no-op scaffold-dispatch case. For bringing existing repos (e.g. Farmer Gracy) into Claudable to chat-edit while they deploy via their own pipeline.
…/etc. backend) Extends static preview mode: a project can declare .claudable/preview.json with a backend (build+run command, env, health path) and proxy prefixes. The preview builds + runs the backend on an internal loopback port and the static server reverse-proxies the configured prefixes (e.g. /api,/d,/settings) to it; project EnvVars override the config env (secrets via the Envs tab). Backend process is tracked + torn down with the preview. Adds the Go toolchain to the Claudable image so Go backends build/rebuild on start.
…'s secrets
The backend sidecar was spawned with { ...process.env }, so an imported/edited
project backend inherited Claudable's own credentials (CLAUDE_CODE_OAUTH_TOKEN,
GIT_TOKEN, COOLIFY_API_TOKEN, GOOGLE_CLIENT_SECRET, AUTH_SECRET, DATABASE_URL).
Now built from a secret-free allowlist base (buildBackendBaseEnv, same principle
as buildAgentEnv) + the project's own preview.json env + Env vars. Explicit Go
var names (no startsWith('GO') — that would leak GOOGLE_*).
…(opt-in) Phase 1 of environment isolation. When PREVIEW_ISOLATION is set and a project declares backend.container in preview.json, the backend is built from its OWN Dockerfile and run in an isolated container (non-root, cap-drop ALL, no-new-privileges, --memory/--cpus/--pids-limit, only the backend port published to loopback, project env only). Claudable drives Docker via a locked-down docker-socket-proxy sidecar (DOCKER_HOST), never the raw socket, so its blast radius is container ops only. Adds the docker CLI to the image. Container is tracked + docker-rm'd with the preview. Off by default (flag lives in .env).
Isolated backend containers attach to PREVIEW_SANDBOX_NETWORK — a dedicated bridge (ICC off) whose DOCKER-USER + INPUT firewall rules block the box's private ranges (host, Claudable, DBs, other previews, cloud metadata) while allowing the public internet + DNS. Closes the network-egress leak from the isolation review.
…g-gated) A framework project can opt into running its dev server in an isolated, egress-locked container via frontend.isolate in .claudable/preview.json. Runs a foreground docker run (reusing the spawn/readiness/log machinery): bind-mounts just the project dir into a node image, non-root, cap-drop ALL, no-new-privileges, mem/cpu/pid limits, sandbox network, clean env. Skipped for DB-backed projects (egress lock would block a box-hosted Postgres) and off unless the flag + the per-project opt-in are both set — so nothing else is affected. Adds DATA_HOST_DIR for host-path bind mounts + frontend container teardown.
…ATABASE_URL env.DATABASE_URL is always set (Claudable's own sqlite URL from compose), so the !env.DATABASE_URL guard never passed and frontend isolation never triggered. Key off the project's provisioned DB (getDatabaseUrl) instead.
…ik reaches it via the host gateway, not loopback)
…Bash The preview isolation gave Claudable a docker socket-proxy (DOCKER_HOST); since the agent runs inside Claudable, a Bash 'docker run -v /:/host' would mount the host root (verified: read the host hostname). The agent never needs Docker — the cross-project guard now denies any container-runtime command, the DOCKER_HOST env, the docker socket, or the proxy port. (/proc/<pid>/environ reads were already blocked by the path guard.)
…TECTURE.md - Frontend isolation now auto-applies to ANY nuxt/next/angular project when PREVIEW_ISOLATION is on (opt-out via frontend.isolate:false); reuses the in-process per-stack dev command (devArgs) so it works across stacks without per-project config. - Each preview start writes .claudable/ARCHITECTURE.md — a plain-language summary of how that project runs (stack, isolation, containers, ports, editing, deploy) that both the user and the agent can read.
… vs the derived family)
An info button opens a modal showing the project's runtime architecture — read from .claudable/ARCHITECTURE.md (regenerated each preview start) via a new GET /api/projects/[id]/architecture, rendered as markdown. Shows stack, isolation + hardening, ports, editing model, and deploy target.
… handleApiError args) Use an inline SVG info icon instead of the unavailable FaInfoCircle, and pass handleApiError its (error, context, message) args. Clean builds caught what incremental builds had masked.
Step 1 of the composition feature: backend-stacks registry (Node/Express, Go, Python/FastAPI — each a starter server + Dockerfile) + databases registry (SQLite/Postgres/MySQL). New-project screen gets Backend + Database pickers; the create API stores the choice in the project's settings JSON (no schema change) and scaffolds the backend into the repo with a .claudable/preview.json backend config. Widened the preview port band to 3710-3739 for full-stack (2-port) projects. Preview wiring (model B: backend as its own service + URL) + managed-DB provisioning + add-later UI are the next steps.
…ice (model B) A framework project with a backend now runs the backend as its OWN hardened, egress-locked container on preview-<slug>-api (published on all interfaces, its own Traefik route + LE cert), derived port (frontend+5000, no 2nd pool slot). CORS_ORIGIN set to the frontend origin; the frontend container gets NUXT_PUBLIC_API_BASE / NEXT_PUBLIC_API_BASE / API_BASE_URL injected so it calls the backend. Backend route torn down with the preview.
…, was colliding with the frontend container
… services) New per-project 'Containers' settings tab + GET/POST/DELETE /api/projects/[id]/containers: lists the project's runtime services (frontend, backend, database) with type/status/URL/description, and add/remove actions (add-later). Managed Postgres is provisioned/removed via Coolify. Folds in the old Database tab (removed) and renames the Git 'Services' tab to 'Deploy' to free the name. The 'i' architecture modal is the read-only sibling.
…n), not just settings.backendType Farmer Gracy's Go backend is configured via .claudable/preview.json (set at import), so the panel missed it. Now shows a Backend card when either settings.backendType OR preview.json backend.container is present; imported static-site backends show the proxied /api URL (not a -api subdomain).
…ATA_DIR) DB Imported backend cards now show the language (Go/Node/Python, detected from the backend dir) instead of 'container · container', and a project whose backend keeps data in DATA_DIR gets an 'embedded (file · in backend)' Database card — honest that it lives in the backend, not a separate managed service.
Security: (S1) scrub the in-process framework dev server env — it was spawned with raw process.env, leaking Claudable secrets to agent-authored server code; now an allowlist + project vars. (S2) refuse to run preview.json backend build/run via sh -c on the HOST when isolation is on (require a container) + validate backend.cwd stays in-project. Bugs: (B1) inject the composed backend URL into the in-process frontend too (DB projects); (B2) actually provision Postgres chosen at project creation; (B5) scaffold backend before persisting settings; (B8) re-serialize the created project with its new settings; (B12) reset ArchitectureModal content on reopen. Dead code: delete unused ProjectDatabaseSettings.tsx + dead ensureDependencies() + unused imports (SDKMessage, Brain, Create/DeleteProjectModal, validateProjectExists).
New in-process MCP tool mcp__images__generate_image: the agent generates images from a prompt (xAI Grok images API) and they're saved into the project's public/generated/ (served at /generated/<file>), returned as paths to use in the app. Key stays in Claudable (never in the agent's scrubbed env) — resolved from the project's XAI_API_KEY/IMAGE_API_KEY Env var, else a global XAI_API_KEY. Only attached + prompted when a key is configured. Generic key lookup so a non-xAI provider can be swapped in later.
…/PNG from magic bytes
…-in + auth) Introduces a capability model: a project CONNECTS to image generation (opt-in), stored as a ProjectServiceConnection(provider:images) with a scoped token + an optional own key (else the shared global key). lib/services/capabilities/images.ts owns connect/disconnect/resolveKey; the MCP tool + agent attach only for connected projects. New /api/projects/[id]/image-capability (GET/POST/DELETE) + a Connect card in the Containers panel. Replaces implicit global-on with real per-project connections — the same auth layer a future network MCP endpoint can reuse.
The chat input is never disabled while a turn runs — you can always type/send. Sending while busy QUEUES the message; queued messages auto-send one-per-turn as each turn finishes (edge-detected on busy->idle). A small 'N queued' indicator with a Clear action shows pending messages. Stop still interrupts the current turn. (ChatInput submit/Enter/send-button no longer gated on isRunning.)
holoduke
added a commit
that referenced
this pull request
Jul 8, 2026
…es + Tier 2 major migrations' (#2) from chore/deps-upgrade into main
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Self-host feature work layered on top of PR opactorai#83 (the prior
feat/skills-opus-nuxt-selfhostsnapshot is the base of this PR, so the diff is just the new work).Highlights
it-ops broker (admin-enabled infrastructure tools for the agent)
infra_health, cross-repo secret-read blocklist + path encoding, secret-key masking, idempotent branch protection, request timeouts, org-scoped user admin, and a fail-closed auth guard.Auth / users
itopsEnabledwith a top-right My Account menu (admins self-enable; admins grant others via User Management). Projects record their creator as owner.Preview isolation (security)
Visual editor
Preview comments
Uploads & preview UX
Test plan
tsc --noEmit); builds green in CI/Docker.No secrets included — only
.env.example; all infra values come from runtime env.🤖 Generated with Claude Code