Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
75 changes: 75 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
- name: Ruff format check
run: uv run ruff format --check .

- name: Verify generated control-plane contract
run: uv run python scripts/generate_control_plane_contract.py --check

test-unit:
name: Unit Tests
runs-on: ubuntu-latest
Expand All @@ -55,6 +58,78 @@ jobs:
- name: Run unit tests
run: uv run pytest tests/unit/ -v --cov=loafer --cov-report=term-missing

control-plane-postgres:
name: Control Plane PostgreSQL Contract
runs-on: ubuntu-latest
needs: [lint]
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: loafer
POSTGRES_PASSWORD: loafer
POSTGRES_DB: loafer_dev
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U loafer -d loafer_dev"
--health-interval 5s
--health-timeout 3s
--health-retries 10
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install dependencies
run: uv sync --all-extras

- name: Run PostgreSQL metadata and RLS migrations
env:
TEST_POSTGRES_URL: postgresql://loafer:loafer@localhost:5432/loafer_dev
run: uv run pytest tests/integration/test_metadata_store.py -v

web-auth:
name: Web & Authentication Contracts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22.16.0'
cache: npm
cache-dependency-path: web/package-lock.json

- name: Install dependencies
working-directory: web
run: npm ci

- name: Check production advisories
working-directory: web
run: npm audit --omit=dev --audit-level=high

- name: Lint and typecheck
working-directory: web
run: npm run lint && npm run typecheck

- name: Verify Better Auth security contracts
working-directory: web
run: npm run test:auth

- name: Build web and BFF routes
working-directory: web
run: npm run build

smoke:
name: Artifact Smoke (clean room)
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
/lib/
/lib64/
parts/
sdist/
var/
Expand Down
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ repos:
# Format Python files in-place; ruff re-exits non-zero so
# pre-commit re-stages the changes and asks you to commit again.
- id: ruff-format
- repo: local
hooks:
- id: web-eslint
name: web eslint --fix
entry: web/node_modules/.bin/eslint --fix
language: system
files: ^web/.*\.(cjs|js|jsx|mjs|ts|tsx)$
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ Notable changes to Loafer are documented here. This project follows

### Added

- An HTTPS-only `loaferd` control plane with a versioned `/api/v1` OpenAPI contract, generated
browser types, typed Python/browser clients, durable commands, sequenced SSE run events, request
auditing, security headers, origin enforcement, and rate limiting.
- Better Auth 1.6.25 integration for admin bootstrap, verified email/password sessions,
organizations and invitations, CLI device authorization, scoped expiring automation keys, and
short-lived audience-bound JWT/JWKS credentials through the Next.js BFF.
- Tenant metadata for workspaces, environments, role-based permissions, connections with opaque
secret references, control commands, and audit events, plus PostgreSQL row-level security
policies as defense in depth.
- Versioned SQLite/PostgreSQL metadata migrations for immutable pipeline versions, runs, stages,
partitions, batches, checkpoints, events, artifacts, schedules, and transactional outbox rows.
- Explicit run, stage, and batch state machines; idempotent run/schedule/cancel commands; leases,
Expand All @@ -31,6 +40,15 @@ Notable changes to Loafer are documented here. This project follows

### Changed

- Metadata migrations now run only through the explicit `loafer metadata migrate` command;
control-plane and durable-worker composition perform read-only schema-version checks instead.
- Pre-commit hooks now auto-fix and re-stage lint changes in staged web JavaScript and TypeScript
files alongside the existing Python Ruff checks.
- `loafer enqueue` now targets `loaferd` by default and never silently falls back to embedded
execution; `loafer run` and embedded enqueueing require an explicit `--local` flag.
- CI now verifies the generated control-plane contract, Better Auth security behavior and web
build under Node.js 22.16, production dependency advisories, and control-plane migrations/RLS
policies against PostgreSQL 16.
- Scheduled callbacks now create durable idempotent run commands; pipeline execution happens only
in a separately started worker process.
- Bounded durable runs stage each transformed batch as an immutable object and commit its metadata,
Expand All @@ -47,6 +65,20 @@ Notable changes to Loafer are documented here. This project follows

### Fixed

- Control-plane mutations and their audit records now commit in one transaction, so an audit
failure rolls back the corresponding run, connection, command, pipeline, or schedule change.
- Pipeline and connection creation endpoints now require `Idempotency-Key`, matching the control
plane's other retryable command routes.
- Synchronous JWKS lookup and signature verification now run outside the FastAPI event loop with
an explicit configurable timeout for authentication-server cache misses and failures.
- Disabled the CDN-backed Swagger UI route that could not render under the control plane's strict
global CSP; the runtime and checked-in OpenAPI contracts remain available.
- PostgreSQL metadata migrations now take an advisory transaction lock, preventing accidentally
concurrent one-shot migration jobs from racing on schema DDL and version records.
- Better Auth integration tests now explicitly enable TypeScript stripping on the pinned Node.js
22.16 runtime, and artifact smoke tests opt into the required local execution mode.
- Anchored Python packaging ignore rules so required web authentication and control-plane client
modules, including the generated browser contract, are tracked by Git and available in CI.
- Cancellation, transform failures, and target failures during bounded file runs now discard
run-scoped temporary output instead of publishing a final partial file.
- CSV encoding detection now scans in bounded chunks instead of allocating the entire source file
Expand All @@ -57,6 +89,10 @@ Notable changes to Loafer are documented here. This project follows

### Known limitations

- Validation, backfill, and connection-test endpoints persist durable control commands, but their
distributed consumers arrive with the NATS/worker-pool work; the HTTP process never executes
these operations inline. The embedded Better Auth SQLite profile requires Node.js 22.16 or
newer, while PostgreSQL is the production authentication profile.
- SQLite metadata is restricted to the embedded profile with one scheduler and one worker; it does
not advertise high availability or distributed claims. PostgreSQL is the authoritative platform
profile. The bundled object-storage adapter is local filesystem storage, not a distributed blob
Expand Down
28 changes: 28 additions & 0 deletions PRODUCTION_READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,34 @@ Exit gate:
- no response, log, event, or OpenAPI schema exposes secret values;
- HTTP requests enqueue/use application commands and never execute pipelines inline.

**Current status:** the Phase 4 control-plane baseline is implemented. Better Auth 1.6.25 owns
users, verified sessions, organizations, invitations, platform-admin bootstrap, device login,
scoped expiring automation keys, and Ed25519 JWT/JWKS issuance. The Next.js BFF validates browser
sessions and API keys before minting a 15-minute audience-bound token; `loaferd` validates that
token over HTTPS and never accepts unsigned tenant headers or cookie-authenticated mutations.

Metadata schema v3 adds Loafer-owned workspaces, environments, permissions, connections with
opaque secret references, durable control commands, and audit events. Every resource repository
query carries an explicit workspace predicate, guessed cross-tenant IDs return 404, role policy is
enforced in the application service, and PostgreSQL installs workspace RLS policies as deployment
defense in depth. The versioned API exposes pipelines, runs, logs/events, connections, schedules,
validate, create-run, cancel, retry, backfill, and connection-test commands. Run creation only
persists queued worker work; the HTTP process does not invoke a pipeline.

The OpenAPI document and generated browser types are checked into source and regenerated in CI.
The Python CLI and web application use typed HTTPS clients over the same `/api/v1` contract, SSE
supports sequence IDs, reconnect, heartbeats, and gap notices, and remote CLI credentials are kept
in the operating-system keyring. Contract tests cover the authorization matrix, guessed IDs,
origin/cookie boundaries, token audience/expiry, rate limits, idempotency, secret redaction, and
HTTPS-only clients. Better Auth integration tests cover disabled public signup, trusted origins,
secure cookies, session replacement/revocation, and credential rate limiting. Metadata v3 and its
RLS policies pass against live PostgreSQL 16.

Distributed consumers for validation, backfill, and connection-test control commands remain Phase
5 work. Those endpoints durably accept work now but do not execute it inside `loaferd`. Production
deployments use PostgreSQL and a non-owner application role configured for RLS; the built-in
SQLite auth/metadata profiles remain development and single-node options.

### Phase 5 — Introduce distributed workers and NATS JetStream

**Goal:** scale and isolate execution without making the queue the source of truth.
Expand Down
55 changes: 40 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ Define a source, transformation, and target; validate the pipeline; then run it
or from a scheduler. Transformations can use SQL, custom Python, multi-step pipelines, or optional
LLM-generated artifacts.

> **Project status:** Loafer ships as a CLI engine with durable single-node scheduling and worker
> recovery. The multi-tenant API, distributed queue/workers, web operations dashboard, and terminal
> dashboard are under active development. The `/studio` web route is a product preview, not a
> connected control plane.
> **Project status:** Loafer ships a CLI engine, durable single-node scheduling/recovery, and the
> authenticated multi-tenant `loaferd` HTTPS control plane. Distributed queue/workers, the connected
> web operations dashboard, and the terminal dashboard remain under active development. The
> `/studio` route is still a product preview.

## What works today

Expand All @@ -23,6 +23,10 @@ LLM-generated artifacts.
- Local scheduling, daemon management, run summaries, and logs
- SQLite/PostgreSQL run metadata, fenced worker leases, durable batch checkpoints, replayable
temporary output, and monotonic run events
- Better Auth sessions, organizations, invitations, device login, scoped automation keys, and
short-lived JWT/JWKS exchange through the Next.js authentication boundary
- Stateless `loaferd` `/api/v1` resources and commands with workspace roles, audit events,
idempotency, SSE reconnect, secret references, OpenAPI, and HTTPS-only clients
- Optional Gemini, OpenAI, Claude, and Qwen providers
- Resource-limited Python transform subprocesses on Linux and macOS
- Declared row-local ETL with bounded batches, per-batch validation, schema policies,
Expand Down Expand Up @@ -62,11 +66,25 @@ docker pull ghcr.io/lupppig/loafer:latest
docker run --rm \
-v "$(pwd):/workspace" \
-w /workspace \
ghcr.io/lupppig/loafer:latest run pipeline.yaml
ghcr.io/lupppig/loafer:latest run pipeline.yaml --local
```

Mount pipeline files under `/workspace`, not `/app`; `/app` is reserved by the image.

## Metadata schema rollout

Prepare durable metadata explicitly before starting `loaferd` or a durable worker. For PostgreSQL,
set the authoritative URL and run the migration as a one-shot deployment job:

```bash
export LOAFER_METADATA_URL="postgresql://loafer:secret@postgres/loafer"
loafer metadata migrate
```

Run the same command without `LOAFER_METADATA_URL` to prepare the embedded SQLite profile. Service
startup never runs DDL: it checks the installed schema version and exits with an actionable error
when migration has not run or the database belongs to a newer Loafer release.

## Quick start

Create `pipeline.yaml`:
Expand Down Expand Up @@ -99,7 +117,7 @@ Run it:
```bash
export DATABASE_URL="postgresql://user:password@localhost/app"
loafer validate pipeline.yaml
loafer run pipeline.yaml
loafer run pipeline.yaml --local
```

Loafer infers connector and transform types from URLs, file extensions, and configuration fields.
Expand Down Expand Up @@ -249,8 +267,10 @@ implemented; `ocr_applied` remains `false` in provenance.
## CLI

```text
loafer run <pipeline.yaml>
loafer login --auth-url https://loafer.example.com
loafer enqueue <pipeline.yaml> --command-key <idempotency-key>
loafer run <pipeline.yaml> --local
loafer enqueue <pipeline.yaml> --local --command-key <idempotency-key>
loafer worker [--once]
loafer validate <pipeline.yaml>
loafer connectors
Expand All @@ -265,6 +285,11 @@ loafer init

Use `loafer <command> --help` for command-specific options.

Remote `loafer enqueue` calls `loaferd` over HTTPS and requires `LOAFER_API_URL`,
`LOAFER_AUTH_URL`, and `LOAFER_WORKSPACE_ID`. `loafer login` stores the device-session credential in
the operating-system keyring and exchanges it for short-lived API JWTs. There is no Unix-socket
protocol and no silent local fallback. Use `--local` explicitly for embedded compatibility mode.

`loafer schedule` and the scheduler daemon only enqueue durable run commands; start `loafer worker`
as a separate process to execute them. The embedded profile defaults to SQLite under `~/.loafer`
and supports one scheduler and one worker. Set `LOAFER_METADATA_URL` to a PostgreSQL URL for the
Expand All @@ -275,20 +300,20 @@ authoritative platform store and `LOAFER_OBJECTS_PATH` to choose the local artif
The production architecture separates clients, control plane, and data plane:

```text
Next.js web/BFF ─┐
├─ Better Auth ─ control-plane API ─ PostgreSQL/outbox ─ NATS JetStream
CLI / TUI ───────┘ ├─ ETL workers
└─ browser workers
Browser ─ Next.js BFF / Better Auth ─┐
CLI / automation ─ signed JWT ───────┴─ HTTPS `/api/v1` `loaferd`
└─ PostgreSQL/outbox ─ NATS JetStream
├─ ETL workers
└─ browser workers
```

The web dashboard and planned terminal dashboard will use the same API, permissions, run events,
metrics, and logs. Workers will run independently so startups can deploy the stack on one host
while larger installations can scale and isolate worker pools.

The full stack is not shipped yet. Durable metadata, leases, fencing, outbox records, and
single-node bounded-batch recovery are implemented; the authenticated API, distributed queue,
tenant authorization, and distributed object store are not. Use the CLI/Docker path for bounded
workloads and do not expose Studio as a production operations surface.
The control interface, authentication boundary, tenant authorization, and durable single-node
state are implemented. NATS transport, distributed object storage, isolated worker pools, and the
connected operator UI are not. Do not expose Studio as a production operations surface yet.

The planned web source uses Crawlee for Python with HTTP/Parsel and Playwright execution profiles.
It will support bounded crawling, authorized authenticated sessions, JavaScript rendering, and
Expand Down
Loading
Loading