Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Self-hosted Function-as-a-Service (FaaS) for homelab and on-premises use. Users
```bash
# Docker (recommended)
docker compose up -d
# → dashboard at http://localhost:8443
# → dashboard at http://localhost:3000 (compose maps host 3000 → container 8443)

# Dev mode (frontend hot-reload + backend auto-restart)
make dev
Expand All @@ -16,14 +16,14 @@ make dev
## Build Commands

```bash
make build # backend binary → build/orva (calls adapters-embed)
make build # backend binary → build/orva (calls adapters-embed + docs-embed)
make build-all # embed UI then build (full release artifact)
make test # cd backend && go test ./...
make lint # cd backend && go vet ./...
make test # go test -count=1 ./... (from repo root)
make lint # go vet ./... (from repo root)
make ui # cd frontend && npm install && npm run build
make embed # build UI, copy dist/ → backend/internal/server/ui_dist/
make cli # static CLI binary → build/orva-cli (current OS)
make cli-all # cross-compile CLI: linux/amd64, linux/arm64, darwin/arm64
make cli # static CLI binary → build/orva (current OS)
make cli-all # cross-compile CLI: linux/{amd64,arm64}, darwin/{amd64,arm64}, windows/{amd64,arm64}
make adapters-embed # sync runtimes/ → backend/cmd/orva/adapters/ (auto-called by build)
make docs-embed # sync docs/reference.md → mcp + frontend (auto-called by build/ui)
make clean # remove build/ and embedded artefacts
Expand All @@ -38,7 +38,7 @@ backend/ Go server (see backend/CLAUDE.md)
internal/ Server packages (config, database, pool, proxy, mcp, …)
runtimes/ Runtime adapter source: node, python
cli/ Slim standalone CLI codebase (see cli/CLAUDE.md)
cmd/orva/ Slim CLI entry point (no server packages — ~12 MB binary)
cmd/orva/ Slim CLI entry point (no server packages — ~20 MB binary)
commands/ Cobra subcommand library — single source of truth for
both binaries (server imports it for its CLI surface)
internal/ Shared utilities accessible to both backend/ and cli/
Expand All @@ -47,7 +47,8 @@ internal/ Shared utilities accessible to both backend/ and cli/
frontend/ Vue 3 dashboard (see frontend/CLAUDE.md)
docs/ Operator and developer documentation (see docs/CLAUDE.md)
scripts/ Installers (install.sh = server, install-cli.{sh,ps1} = CLI),
Docker entrypoint, systemd unit, OpenRC unit
Docker entrypoint (entrypoint.sh); the systemd + OpenRC units
are emitted inline by install.sh, not separate files
test/ Shell-based integration test suite (see test/CLAUDE.md)
cli/ CLI-specific tests (build matrix, install-cli, upgrade, command-tree)
install/ Server-install e2e harness (privileged systemd-in-docker)
Expand All @@ -58,7 +59,7 @@ Dockerfile Multi-stage image (dev and production — single file)

## Data & Configuration

- **Data dir**: `/var/lib/orva` (Docker volume `orva_data`) — contains `orva.db` (SQLite WAL) and `functions/<id>/versions/`
- **Data dir**: `/var/lib/orva` (Docker volume `orva-data`) — contains `orva.db` (SQLite WAL) and `functions/<id>/versions/`
- **Server config**: env vars or `/etc/orva/config.yaml`; full reference in `docs/CONFIG.md`
- **CLI config**: `~/.orva/config.yaml` with `endpoint` and `api_key`

Expand Down Expand Up @@ -101,9 +102,9 @@ Every server binary stamps three variables via `-X` ldflags at link time. They f

| Variable | Source | Example |
|---|---|---|
| `internal/version.Version` | git tag on release; `git describe` in dev | `v2026.05.15` |
| `internal/version.Commit` | `git rev-parse --short HEAD` (CI: `${GITHUB_SHA::7}`) | `1be3399` |
| `internal/version.BuildTime` | `date -u +%Y-%m-%dT%H:%M:%SZ` at link time | `2026-05-15T14:20:34Z` |
| `backend/internal/version.Version` | git tag on release; `git describe` in dev | `v2026.06.14` |
| `backend/internal/version.Commit` | `git rev-parse --short HEAD` (CI: `${GITHUB_SHA::7}`) | `1be3399` |
| `backend/internal/version.BuildTime` | `date -u +%Y-%m-%dT%H:%M:%SZ` at link time | `2026-05-15T14:20:34Z` |

Go silently ignores unknown `-X` targets, so renaming the version package or any of its variables MUST be done in lock-step across `Makefile`, `Dockerfile`, and `.github/workflows/release.yml` — otherwise the binary ships with defaults (`"dev"` / `"unknown"`) and the dashboard's Build info card lights up red flags.

Expand All @@ -114,4 +115,4 @@ Go silently ignores unknown `-X` targets, so renaming the version package or any
- **UI is embedded** in the Go binary via `//go:embed ui_dist`; `make build` alone reuses the last embedded snapshot. Run `make build-all` (or `make embed` first) to pick up frontend changes.
- **nsjail required on Linux** for sandbox invocations; the server starts without it but every invocation fails until it is installed.
- **Firewall (nft) probe is lazy** — the nftables package does not probe on import; it probes on first use via `sync.Once`, so CLI invocations do not trigger nft warnings.
- **Docs single source:** `docs/reference.md` is the canonical Orva reference markdown. `make docs-embed` ships copies to `backend/internal/mcp/reference.md` (embedded by the `get_orva_docs` MCP tool) and `frontend/public/docs.md` (served at `/docs.md` and read by the dashboard's Copy as Markdown button). Both consumers serve identical bytes.
- **Docs single source:** `docs/reference.md` is the canonical Orva reference markdown. `make docs-embed` ships copies to `backend/internal/mcp/reference.md` (embedded by the `get_orva_docs` MCP tool), `frontend/public/docs.md` (served at `/docs.md` and read by the dashboard's Copy as Markdown button), and `cli/commands/reference.md` (embedded into the CLI, served by `orva docs`). All three consumers serve identical bytes.
15 changes: 7 additions & 8 deletions backend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ go vet ./...
| `metrics` | Prometheus-text counters + histograms (no external deps, atomic ops) |
| `secrets` | AES-256-GCM encrypted secrets per function |
| `scheduler` | Cron runner (`robfig/cron/v3`) |
| `mcp` | MCP server (go-sdk); 70 operator-management tools OR channel-mode (one tool per bundled function, invoke-only). Auth accepts API keys, OAuth 2.1 access tokens, OR channel tokens. |
| `mcp` | MCP server (go-sdk); 72 operator-management tools OR channel-mode (one tool per bundled function, invoke-only). Auth accepts API keys, OAuth 2.1 access tokens, OR channel tokens. |
| `oauth` | OAuth 2.1 authorization server (RFC 7591 DCR + RFC 8414 metadata + PKCE S256 + RFC 8707 resource indicators + RFC 7009 revocation). Lets claude.ai/ChatGPT add `/mcp` as a custom connector via the browser. Connected apps + sessions managed at `/api/v1/oauth/connected-apps` and `/api/v1/auth/sessions` and surfaced in the dashboard's Settings page. DCR default scope is `read invoke write admin`. |
| `auth` | Shared `Principal` type (Kind=api_key / oauth / channel + ID/Label/Perms/Channel). Both REST middleware and MCP auth resolve the inbound bearer to a `*Principal`; downstream code (activity log, MCP tool registration) consumes the Kind directly. |
| `trace` | Causal-trace collector + span lifecycle (W3C `traceparent` interop, outlier detection). See `docs/TRACING.md`. |
Expand All @@ -50,7 +50,6 @@ go vet ./...
| `server` | HTTP router + middleware chain + all handlers |
| `server/events` | SSE event hub + outbound webhook fanout |
| `server/handlers` | One file per resource group; `respond/` sub-package |
| `cli` | Shared `Client` + `Config` for CLI subcommands |
| `backup` | `SnapshotDB` / `ArchiveTo` / `RestoreFrom` helpers |
| `version` | Single source of truth for the version string |
| `ai` | In-product AI chat assistant. `Manager` (service layer) wires the SQLite store, the secrets cipher (provider-key encryption), the in-process tool registry, the embedded Bifrost LLM gateway (`ai/llm`), and the agentic loop (`ai/agent`). Served at `/api/v1/ai/*` by `server/ai_handler.go` (SSE for chat/approval). The agent's `defaultSystemPrompt` const lives in `ai/manager.go` — it's a Go **raw string, so it must stay backtick-free** (escape any fenced-code examples by description, not literal ```). |
Expand All @@ -59,21 +58,21 @@ go vet ./...

The canonical UUIDv7 generator (`ids`) and HTTP client (`client`) live at **repo-root** `internal/ids/` and `internal/client/` — shared with the slim CLI codebase, not under `backend/internal/`.

## CLI Commands (`cmd/orva/`)
## CLI Commands (`cli/commands/`)

All Cobra subcommands share one binary with the server. `orva serve` starts the daemon; every other command is a CLI client that reads `~/.orva/config.yaml`.
All Cobra subcommands share one binary with the server. `orva serve` starts the daemon; every other command is a CLI client that reads `~/.orva/config.yaml`. The command library lives at repo-root `cli/commands/` (NOT under `backend/cmd/orva/`, which holds only `main.go`/`serve.go`/`setup.go`/`init_cmd.go` + the embedded `adapters/`); both binaries register it via `commands.NewRoot()`. See `cli/CLAUDE.md`.

Key files: `deploy.go`, `diff.go`, `functions.go`, `invoke.go`, `logs.go`, `cron.go`, `kv.go`, `jobs.go`, `secrets.go`, `webhooks.go`, `routes.go`, `keys.go`, `system.go`, `activity.go`, `completion.go`.
Key files: `deploy.go`, `deployments.go`, `diff.go`, `rollback.go`, `functions.go`, `invoke.go`, `logs.go`, `executions.go`, `cron.go`, `kv.go`, `jobs.go`, `secrets.go`, `webhooks.go`, `routes.go`, `dns.go`, `firewall.go`, `fixtures.go`, `channels.go`, `traces.go`, `pool.go`, `keys.go`, `system.go`, `backup.go`, `activity.go`, `chat.go`, `docs.go`, `completion.go`.

## Key Patterns

**Handler responses**: always use `respond.JSON(w, status, val)` / `respond.Error(w, status, "SLUG", "message")` from `server/handlers/respond/`.
**Handler responses**: always use `respond.JSON(w, status, val)` / `respond.Error(w, status, "SLUG", "message", requestID)` from `server/handlers/respond/` (the last arg is the request ID, often `RequestID(r.Context())` or `""`).

**Invocation funnel**: HTTP, cron, jobs, and F2F calls all go through `Worker.Dispatch()` (sync response) or `Worker.DispatchEx()` (multi-frame streaming). Never invoke nsjail directly from handlers.

**Async DB writes**: execution rows use `database.AsyncInsertExecution*` batch writers — no synchronous DB calls on the hot proxy path.

**Name resolution**: functions can be referenced by UUID or by name. Use `resolveFnID(db, nameOrID)` from `handlers/functions_helpers.go`.
**Name resolution**: functions can be referenced by UUID or by name. Use the handler method `(h *FunctionHandler) resolveFnID(idOrName string) (string, bool)` in `handlers/functions.go` (sibling copies on `FixtureHandler` / `KVOperatorHandler` / `InboundWebhookHandler`).

**Streaming wire protocol**: `response_start` → `chunk` (base64 body data) → `response_end` frames over the worker's stdin/stdout pipe. `proxy.Forward()` owns the write-loop.

Expand All @@ -98,4 +97,4 @@ SQLite WAL mode. All migrations in `internal/database/migrations.go` — additiv
- **AI conversation editing is destructive-tail:** editing or deleting a chat message (`EditMessage` / `DeleteMessage` in `server/ai_handler.go`, backed by `database.DeleteMessagesFromSeq`) truncates the conversation at that message's `seq` — it deletes that message and every message + tool call after it, then (for edit) re-runs the turn. There is no branching history; the tail is gone. `Regenerate` is the same truncate-then-rerun on the last assistant turn.
- **AI turns are one-per-conversation:** the `ai.Manager` holds a keyed try-lock (`tryLockConv`/`unlockConv`) acquired by every mutating entry point (Chat, Resume, RegenerateLast, EditAndResend, DeleteMessageFrom). An overlapping turn on the same conversation is rejected — SSE `error` for streaming paths, `ai.ErrConversationBusy` → 409 for the JSON delete. `database.InsertMessage` assigns `seq` atomically inside the INSERT (`MAX(seq)+1` subquery); never split it back into a SELECT-then-INSERT.
- **AI gateway lifecycle:** `ai.Manager.Close()` releases the embedded Bifrost pools and is called from `Server.Shutdown` (via `s.router.ai`). The gateway is built lazily and rebuilt on provider-config change (`invalidateClient`).
- **Docs single source:** `docs/reference.md` is the canonical Orva reference markdown (~53 KB). `make docs-embed` syncs it to `backend/internal/mcp/reference.md` (embedded by the `get_orva_docs` MCP tool) and `frontend/public/docs.md` (served at `/docs.md` for the dashboard's Copy as Markdown button). Edit the canonical file then run `make docs-embed`; the Vue Docs page is the rendered version (separate templates) and must be updated alongside if content changes.
- **Docs single source:** `docs/reference.md` is the canonical Orva reference markdown (~68 KB). `make docs-embed` syncs it to `backend/internal/mcp/reference.md` (embedded by the `get_orva_docs` MCP tool), `frontend/public/docs.md` (served at `/docs.md` for the dashboard's Copy as Markdown button), and `cli/commands/reference.md` (embedded into the slim CLI, served by `orva docs`). Edit the canonical file then run `make docs-embed`; the Vue Docs page is the rendered version (separate templates) and must be updated alongside if content changes.
3 changes: 2 additions & 1 deletion cli/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ cli/
├── upgrade.go # `orva upgrade` (self-update via go-selfupdate)
├── webhooks.go # `orva webhooks …`
├── commands_test.go # command-tree + flag-presence tests
├── chat_test.go # chat SSE drive + approval-flow tests (httptest)
├── chat_test.go # chat SSE drive + approval-flow + idle/EOF tests (httptest)
├── upgrade_test.go # `orva upgrade` decision logic + asset-filter tests
├── reference.md # GENERATED — embedded by docs.go (make docs-embed)
└── theme/ # lipgloss color palette (theme.New(enabled))
```
Expand Down
2 changes: 1 addition & 1 deletion docs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Human-maintained reference documentation. Keep these in sync when changing API s
| `SECURITY.md` | Threat model, nsjail sandbox isolation, network firewall (nftables) |
| `SUPPORT.md` | Support matrix — distros, kernels, container runtimes |
| `TRACING.md` | Causal trace model, propagation, W3C interop, outlier detection |
| `reference.md` | **Canonical** Orva reference (~53 KB GFM markdown) — single source of truth shipped to the dashboard's Copy-as-Markdown button (via `frontend/public/docs.md`) and the `get_orva_docs` MCP tool (via `backend/internal/mcp/reference.md`). `make docs-embed` syncs both copies. Uses `{{ORIGIN}}` placeholders that consumers substitute at runtime. |
| `reference.md` | **Canonical** Orva reference (~68 KB GFM markdown) — single source of truth shipped to the dashboard's Copy-as-Markdown button (via `frontend/public/docs.md`), the `get_orva_docs` MCP tool (via `backend/internal/mcp/reference.md`), and the slim CLI's `orva docs` command (via `cli/commands/reference.md`). `make docs-embed` syncs all three copies. Uses `{{ORIGIN}}` placeholders that consumers substitute at runtime. |

## Update Triggers

Expand Down
2 changes: 1 addition & 1 deletion frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ After `npm run build`, run `make embed` from the repo root to copy `dist/` into
- Dev proxy: `vite.config.js` proxies `/api` and `/auth` to `http://localhost:8443`. Direct `/fn/`, `/webhook/`, and `/metrics` calls in dev must be made to `:8443` directly — they are not proxied through Vite.
- `src/stores/events.js` opens a persistent SSE connection on mount and reconnects automatically on drop. Dashboard widgets subscribe to this store — they do not open their own connections.
- All AI prompt and clipboard operations (`aiPrompts.js`) are purely client-side — no source code is sent over the network.
- The `Editor.vue` test pane sends requests through the backend (`POST /api/v1/functions/{id}/invoke`) rather than directly to `/fn/` — this ensures auth and capture still apply.
- The `Editor.vue` test pane invokes the function directly at `/fn/<id>` via `invokeFunctionFull` (the `fnClient` in `src/api/client.js`, baseURL `/fn`) so the method/path/headers/body from the Postman-style pane round-trip exactly. `fnClient`'s request interceptor still injects the `X-Orva-API-Key` header, and all `/fn/` traffic passes through the backend proxy, so auth + execution capture still apply. (Note `/fn/` is NOT under `/api/v1`, so it needs the separate client.)
- **AI streaming reactivity (load-bearing):** `stores/ai.js` tracks the streaming assistant message by **index** (`curIdx`) and writes every delta back through the reactive array via `patchAssistant()` (rebuilds `parts` immutably, then `timeline.value[curIdx] = next`). Never hold a raw object reference and mutate `parts[i].text +=` — Vue 3 tracks the array proxy, not the raw ref, so per-token mutations silently fail to re-render. The same index-write rule applies to `tool_result` frames.
- **AI markdown rendering:** `MessagePart.vue` splits markdown into ordered segments so top-level fenced code becomes a real `<CodeBlock>` while prose stays HTML; parsing is throttled to ~12/s (leading + trailing edge) during streaming. Tables inherit the body font size (no shrink) so tabular output matches prose; the system prompt steers the model toward prose/bullets and reserves tables for genuinely tabular data.
6 changes: 3 additions & 3 deletions scripts/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ Support scripts for deployment and installation. None of these are called by the

## Gotchas

- `entrypoint.sh` **always overwrites** `adapter.js` / `adapter.py` from the image on every container start — this ensures runtime upgrades roll out even when the user mounts a persistent `orva_data` volume.
- `entrypoint.sh` **always overwrites** `adapter.js` / `adapter.py` from the image on every container start — this ensures runtime upgrades roll out even when the user mounts a persistent `orva-data` volume.
- `install.sh` embeds the systemd/OpenRC units and `uninstall.sh`; the bare-metal install writes them to `$PREFIX/share/orva/scripts/` and the generated uninstaller to the same path. Edit the heredocs in `install.sh` — there is no separate unit file.
- `install.sh --cli-only` installs only the `orva` CLI binary to `/usr/local/bin/orva` — no systemd unit, no rootfs, no service user. Use this on operator laptops or CI runners that talk to a remote Orva over HTTPS.
- Mode/option precedence is flag > env > interactive prompt > default. Key knobs: `--version`/`ORVA_VERSION` (pin a release), `--dry-run`/`ORVA_INSTALL_DRYRUN=1` (detect only), `--no-pkg`/`ORVA_NO_PKG=1` (skip system packages), `--runtime`/`ORVA_DOCKER_RUNTIME` (force the Docker runtime), `ORVA_SKIP_VERIFY=1` (bypass checksum verification — air-gapped mirrors only; verification is fail-closed otherwise).
- Downloaded assets (orva, nsjail, rootfs, CLI) are SHA-256 verified against `checksums.txt`; a missing checksum **aborts** the install unless `ORVA_SKIP_VERIFY=1`.
- Mode/option precedence is flag > env > interactive prompt > default. Key knobs: `--version`/`ORVA_VERSION` (pin a release), `--dry-run`/`ORVA_INSTALL_DRYRUN=1` (detect only), `--no-pkg`/`ORVA_NO_PKG=1` (skip system packages), `--runtime`/`ORVA_DOCKER_RUNTIME` (force the Docker runtime). There is **no** checksum-bypass env var — `ORVA_SKIP_VERIFY` is referenced in a stale `install.sh` comment but is not implemented.
- Downloaded assets (orva, nsjail, rootfs, CLI) are SHA-256 verified against `checksums.txt`. A checksum **mismatch** aborts the install. A *missing* checksum entry only warns and proceeds in `install.sh` (`verify()` is fail-open on a missing entry); `install-cli.sh` is stricter and aborts when the entry is missing.
- `build-rootfs.sh` produces large tarballs (~hundreds of MB); run only when updating the rootfs base image or adding system libraries.
- Cross-distro installer tests: `test/install/matrix.sh` (fast, unprivileged — shellcheck + POSIX parse + dry-run + real CLI install across 6 distros) and the privileged systemd-in-docker harness under `test/install/`. CI: `.github/workflows/install-e2e.yml`.
Loading
Loading