Skip to content

Self-host: it-ops broker, per-user auth, preview isolation, visual editor + comments#2

Open
holoduke wants to merge 162 commits into
feat/skills-opus-nuxt-selfhostfrom
feat/selfhost-itops-editor-comments
Open

Self-host: it-ops broker, per-user auth, preview isolation, visual editor + comments#2
holoduke wants to merge 162 commits into
feat/skills-opus-nuxt-selfhostfrom
feat/selfhost-itops-editor-comments

Conversation

@holoduke

@holoduke holoduke commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Self-host feature work layered on top of PR opactorai#83 (the prior feat/skills-opus-nuxt-selfhost snapshot 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)

  • In-process MCP broker giving the agent real, scoped write access to Gitea (repos, files, admin: collaborators/branch-protection/org-teams), Coolify (apps + projects: list/restart/deploy/env, create/delete) and Traefik (dynamic routes). AWS/IAM stays propose-only.
  • Per-user, requester-based: attaches only when the user running the agent has it-ops enabled — never leaks to another user in the same project. Credentials run in the Claudable process and never enter the (scrubbed) agent env.
  • Hardened after an adversarial review: SSRF guard on 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

  • Per-user itopsEnabled with a top-right My Account menu (admins self-enable; admins grant others via User Management). Projects record their creator as owner.

Preview isolation (security)

  • Fixed a cross-project preview leak (port reuse): atomic port reservation + boot-clear of stale state, then the definitive fix — per-project preview subdomains whose Traefik route is rewritten to the live port each start (LE cert via DNS-01). Readiness now probes the local dev server and the cert is pre-warmed.

Visual editor

  • Edit mode: click-to-select elements in the preview, live-edit text + CSS via an inspector, "Apply to code" through the agent.

Preview comments

  • Comment mode: Figma-style pinned, per-route review comments (Claudable-only, never in the app source), with author name/avatar, resolve/delete, and project-wide clear-all.

Uploads & preview UX

  • Chunked large-file uploads (defeats the ~10MB body cap) with progress + non-blocking errors.
  • Device modes: desktop / tablet / phone + portrait↔landscape rotate.

Test plan

  • Type-checks clean (tsc --noEmit); builds green in CI/Docker.
  • it-ops tools verified end-to-end against live Gitea/Coolify/Traefik (create→verify→cleanup); credential isolation proven at runtime.
  • Comments API verified (create/list/resolve/clear + per-route scoping).
  • Per-project preview subdomain verified (route written, HTTP 200, valid LE cert, stop withdraws route).
  • Manual UI pass of the visual editor + comment pin placement in a browser.

No secrets included — only .env.example; all infra values come from runtime env.

🤖 Generated with Claude Code

holoduke and others added 30 commits June 24, 2026 15:54
- 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
… 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
…rver), keep SSE always-on (stops connect/disconnect thrash)
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.
holoduke added 29 commits July 2, 2026 12:59
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.
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.
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.
… 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.
…-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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant