Skip to content

Repository files navigation

🧭 Zeroth — From Repository to Verified Deployment

Zeroth takes a public repository URL and answers the question every config generator skips: does this actually deploy? It fingerprints the repo, judges whether Zerops can run it at all, writes the platform configuration — and, if you ask, provisions a real project, deploys, reads the logs, and repairs the config when the build fails.

Most generators hand you plausible YAML. Plausible YAML that does not boot is worse than none, so Zeroth proves it.

Live demo:web-2b21-8080.prg1.zerops.app · Walkthrough:youtu.be/SF26EZozazY

The Zeroth landing screen: a repository URL field, and the four constraints every run is held to

One field. The constraints under it are enforced at submit time, not promised.


Table of Contents


Why Zeroth

Deployment configuration is usually generated blind: a model reads a repository, emits YAML, and nobody finds out whether it works until a human pastes it into a real platform. Zeroth closes that loop. It treats a generated config as a hypothesis and a real deployment as the test — schema validation, project import, and runtime boot are three separate failure classes, each caught at the cheapest level it can be caught at, with a repair loop behind them.

Two phases. The first is free, provisions nothing, and finishes in seconds. The second only happens when you ask for it.


Feature Matrix

#FeatureTypeComplexity driver
1Deployability Verdict⚙️ DeterministicFingerprint evidence judged against emittable runtimes — no model call
2Config GenerationModelFacts-only reasoning → JSON manifest → schema validation → Jinja render
3Real Verification Runs⚙️ DeterministicLive project import, deploy, log read, teardown in finally
4Diagnose → Patch → RedeployModelFailure classification, log-grounded diagnosis, bounded retry (max 2)
5Live Run Streaming⚙️ DeterministicSSE relay over Valkey fan-out, with replay for late connections
6Isolated Credential Handling⚙️ DeterministicShort-TTL delete-on-read keys, per-run zcli session isolation

The Pipeline

flowchart TD
A[Repository URL] --> B{Admission control}
B -->|host allowlist, size cap,<br/>private addresses refused| C[Shallow clone]
B -->|rejected| X[422 at submit — no job created]
C --> D[Fingerprint<br/>facts + evidence]
D --> E{Compatibility verdict}
E -->|deployable| F[Analyze — model reasons over facts]
E -->|needs changes| F
E -->|not deployable| Y[Stop with named blockers]
F --> G[Manifest JSON]
G --> H{Schema valid?}
H -->|no — failure class 1| F
H -->|yes| I[Jinja render<br/>zerops-project-import.yaml + zerops.yaml]
I --> J([READY — config in hand, nothing provisioned])
J -.you press Try it out.-> K[Real Zerops project]
K --> L{Import ok?<br/>failure class 2}
L -->|no| R[Diagnose → patch]
L -->|yes| M[Deploy, read logs, verify]
M --> N{Boots?<br/>failure class 3}
N -->|no| R
R -->|max 2 attempts| K
N -->|yes| O[Verified bundle + DEPLOYMENT.md]
Loading

The three stage cards — analyze, generate, verify — each collapsed with its own status, above the pathfinder replay timeline

The same three stages in the UI. Each collapses to its own status, and the replay timeline behind them keeps every event a run emitted.

Why verification is opt-in

Deploying costs minutes and credits, and most people want to read the configuration before anything is provisioned on their behalf. Stopping at READY makes the common path fast and free, and turns the expensive part into a deliberate act.

Why failures are classified

A schema error costs milliseconds and no provisioning. An import rejection costs one API call. A runtime failure costs a full build. Catching each at the cheapest possible level is the difference between a demo and a tool.


Model-Driven Stages

Analyze — configuration from facts, never from source

zeroth/worker/fingerprint.py extracts facts from manifests, lockfiles, docker-compose.yml, Dockerfile and .env.example — each carrying the evidence that produced it. Only those facts reach the model. No repository source code leaves the worker, and every generated service can answer why is this here? with a filename.

The model emits JSON, which is validated against a local schema (zeroth/worker/manifest_schema.py) before a single Jinja template renders.

sequenceDiagram
participant W as worker
participant FP as fingerprint.py
participant LLM as Groq / EURI
participant S as manifest_schema.py
participant T as Jinja templates
W->>FP: clone dir
FP-->>W: facts + evidence (no source)
W->>LLM: facts only → "how should this deploy?"
LLM-->>W: manifest JSON
W->>S: validate
S--xW: invalid → re-ask (failure class 1)
S-->>W: valid
W->>T: render
T-->>W: zerops-project-import.yaml + zerops.yaml
Loading

The generated zerops-project-import.yaml, marked verified, beside the services panel

What the render produces. The header is not decoration — it names the repository it came from and the command that reproduces it.

The "Why these services" panel: the api service traced back to language, framework, server and port

Every generated service traces back to the evidence that produced it. This is what "facts, not source" buys you — a configuration that can be argued with.

Repair — diagnosis grounded in real logs

When a verification attempt fails, the failure is classified, the build log is captured into the runs row, and the model is asked to diagnose and patch against that log rather than against a guess. Bounded to two attempts, because an unbounded repair loop is a credit burner, not a feature.

flowchart LR
A[Attempt fails] --> B[Classify: schema / import / runtime]
B --> C[Capture build log + verification payload]
C --> D[Model diagnoses against log]
D --> E[Patched config]
E --> F{Attempt < 2?}
F -->|yes| G[Redeploy]
F -->|no| H[Report with attempt history]
Loading

⚙️ Deterministic Stages

1. Is this repository even deployable? (zeroth/worker/compatibility.py)

That is the first question anyone actually has, so it is the first one Zeroth answers — before spending a model call on how to deploy it.

VerdictDeploy buttonMeaning
DeployableenabledRuns on Zerops as it stands
Needs changes (advisory)"Deploy anyway"May still succeed — inferred values are shown for judgement
Needs changes (fatal)disabledThe build cannot succeed as-is (bad encoding, no manifest anywhere); running would only prove it
Not deployabledisabledNo Zerops runtime, or a library rather than an application

When anything actionable is found, Zeroth writes a fix prompt — the findings, their evidence, and the platform constraints that matter — ready to paste into your own coding agent. On request it goes one step further: Draft the fix with AI produces a concrete unified diff for you to apply. That button is the single exception to "no source code reaches the model" — it reads only the files the findings cite, says so on the label, and hands the diff to you. Zeroth never writes to your repository either way.

The deployability verdict: "Deployable with 1 change", two findings, and the generated fix prompt ready to copy

An advisory verdict — deployable, with one thing worth knowing first. The fix prompt below the findings is generated from those findings and their evidence, and goes straight into whatever agent you already use.

It checks runtime support and version availability, whether a dependency manifest exists, whether anything declares how to start the application, port binding, and containerisation. The finding that matters most in practice is a database in the dependencies with no connection string read from the environment — the exact shape of application that works on a laptop and fails on every platform.

This stage is deliberately not a model call. These are the checks that can be decided from evidence, so the stage costs nothing, cannot invent a blocker, and is testable without an API key. Genuinely ambiguous questions are left to the analyze stage rather than guessed at here.

2. Where a verification run lands (zeroth/worker/pathfinder.py)

TargetWhat happensTeardown
Throwaway projectProvisioned on Zeroth's own accountAlways destroyed
Your Zerops accountProvisioned with a token you supply for that one runKept if it came up; failed attempts always destroyed

The token is read once from a short-TTL key and deleted on read — never written to the database, the logs, or the downloadable bundle — and each run gets an isolated zcli session, because zcli login persists a session file rather than authenticating per command. Without that isolation a run against your account would overwrite Zeroth's own session, and concurrent runs would race for the same credentials.

sequenceDiagram
participant U as Browser
participant API as api
participant KV as cache (Valkey)
participant W as worker
participant Z as Zerops platform
U->>API: POST /verify {target: account, token}
API->>KV: store token under short-TTL key
API->>KV: enqueue job
KV-->>W: job
W->>KV: GETDEL token (read once, then gone)
W->>Z: isolated zcli session → import project
Z-->>W: build + runtime logs
W->>KV: publish stage events
KV-->>API: fan-out
API-->>U: SSE stream (with replay)
W->>Z: teardown in finally (unless kept)
Loading

3. Live run streaming (zeroth/bus.py)

Stage transitions are published to Valkey and relayed to the browser over SSE, with a replay buffer so a page that connects late still sees the run from the top. A finished run is rebuilt from the stored record rather than the event stream, so a shared link keeps working long after the buffer has expired.

The event log transcript: every stage transition from cloning to verified, timestamped

The whole run as it was emitted, timestamped. Note the four-and-a-half minute gap at building and deploying api — a real platform build, not a spinner.


System Architecture

flowchart TB
subgraph Public["Public — zerops.app subdomains"]
WEB["web · alpine/python@3.12<br/>showcase, live run view, generated config"]
API["api · ubuntu/python@3.12<br/>job intake, SSE relay, bundle download"]
end
subgraph Private["Private network only"]
WK["worker · ubuntu/python@3.12<br/>clone, fingerprint, assess,<br/>analyze, deploy, repair"]
DB[("db · postgresql@16<br/>jobs, runs, artifacts")]
CACHE[("cache · valkey@7.2<br/>queue, fan-out,<br/>rate limits, concurrency cap")]
end
Browser --> WEB
Browser --> API
API --> DB
API --> CACHE
CACHE --> WK
WK --> DB
WK --> CACHE
WK -.zcli / provider.-> TARGET[Target Zerops project]
Loading

Only web and api are public; everything else talks over the private network.

web is a runtime rather than type: static — the static base 502'd with no logs across several attempts, and serving the same files from python -m http.server was a confirmed path rather than a guess. The reasoning is kept in zerops.yaml next to the setup it explains.

Because web is served from its own origin, the browser cannot reach the API by relative path. web/config.js carries the API base URL — checked in with a localhost default for development, and overwritten at build time with the deployed api subdomain.


Database Schema

erDiagram
jobs ||--o{ runs : "has attempts"
jobs ||--o{ artifacts : "produces"
jobs {
string id PK
string repo_url
string status
string stage_detail
json fingerprint
json compatibility
json manifest
string verify_target
string provider
bool verified
string live_url
string kept_project_id
bool is_gallery
timestamptz created_at
timestamptz finished_at
}
runs {
string id PK
string job_id FK
int attempt_no
string phase
string failure_class
text failure_message
text diagnosis
text patch_summary
string zerops_project_id
text build_log
json verification
}
artifacts {
string id PK
string job_id FK
string kind
string filename
text content
}
Loading

runs is the attempt log: one row per deploy attempt, carrying its failure class, the diagnosis the model produced, and the patch it applied — which is what makes the repair loop auditable after the fact.


Verification with teeth

The fingerprint reads GET routes out of the sources (literal, parameter-free paths only), and the post-deploy probe walks every one of them — application checks, not just "the port answers". When a probe fails, the service's runtime logs are fetched and fed to the repair loop, so a diagnosis reads a traceback instead of guessing about a URL. Every repair records a unified diff of what it changed, persisted per attempt and rendered in the run — the AI's change as evidence, not prose.

The "Deployment verified" card: healthy, one attempt, ephemeral environment, 10 of 10 checks passed

The claim, with its receipts — and the admission that the URL it answered on is already gone. The evidence is the trail, not a live link.

Pathfinder replay

Every state transition a run makes is persisted as a structured event, so a finished run replays as a timeline — offsets, glyphs, failures and repairs in order — long after the live stream is gone. The run page rebuilds the whole story from the stored record: verdict, evidence, attempts, configuration, and what got torn down.

Bring your own key

A run can carry its own OpenAI-compatible LLM key (OpenAI, Groq, OpenRouter, or any compatible endpoint — more presets coming). The key is used for that run only: held under a short TTL, read by the worker, discarded when the run settles, never written to the database, artifacts or logs. Runs on the house keys get a per-run token budget; BYOK runs are exempt — your key runs until your provider says otherwise.

HTTP API

MethodPathPurpose
POST/api/jobsSubmit a repository. Rejects unsupported hosts and oversized repositories here, before any job exists
GET/api/jobs/{id}Full record: fingerprint, verdict, manifest, artifacts, attempts
POST/api/jobs/{id}/verifyDeploy a generated configuration. {"target": "ephemeral"} or {"target": "account", "token": "..."}
GET/api/jobs/{id}/eventsSSE stream, with replay for a browser that connects late
GET/api/jobs/{id}/bundleDownloadable bundle
GET/api/galleryPublic completed runs
GET/healthzLiveness

Tech Stack

LayerTechnology
APIFastAPI + Uvicorn (Python 3.12)
WorkerStandalone Python process, Valkey-driven queue
FrontendStatic HTML/JS + Tailwind, served by python -m http.server
DatabasePostgreSQL 16 via SQLAlchemy 2 + psycopg
Queue & eventsValkey 7.2 (job queue, SSE fan-out, rate limits, concurrency cap)
TemplatingJinja2 (import.yaml, zerops.yaml, DEPLOYMENT.md)
ValidationPydantic 2 + jsonschema
ModelsGroq or EURI (analyze + repair stages)
Deploy providerzcli, or a simulated provider for offline runs
HostingZerops (5 services, private network, subdomains on web/api)

Getting Started

# provision the whole project — 5 services, wired
zcli project project-import import.yaml

Then set the credentials. Set them as project-level environment variables, and note the gotcha: import.yaml declares EURI_API_KEY, GROQ_API_KEY and ZCLI_TOKEN under envSecrets so the names are versioned without the values. Those are service-scoped and service scope shadows project scope — an empty service-level variable silently wins over a populated project-level one, and the application reports no provider configured. Either fill the values in at the service level, or delete the empty service-level entries so the project values resolve.

Tests

npm i -D @playwright/test
npx playwright install chromium # or CHROME_PATH=/path/to/chrome
BASE_URL=https://<web-subdomain> npx playwright test

The E2E suite is read-only by design — it never presses deploy, so running it costs nothing and cannot leave infrastructure behind.

Environment Variables

VariableNeeded for
GROQ_API_KEY or EURI_API_KEYThe analyze and repair stages. Without one, a run fails at analyzing
ZCLI_TOKENDeploying to the throwaway project. Not needed for the user's-own-account path
PATHFINDER_PROVIDERsimulated (default) or zcli

Local Development

pip install uv
uv pip install --system -r requirements.txt
cp .env.example .env # PATHFINDER_PROVIDER=simulated needs no credentials
uvicorn zeroth.api.main:app --reload --port 8000 # terminal 1
python -m zeroth.worker.main # terminal 2
python -m http.server 5173 -d web # terminal 3
python -m zeroth.scripts.seed_gallery # optional: sample runs

web/config.js already points at http://localhost:8000, so the UI on 5173 reaches the API without further configuration.

The simulated provider runs the whole pipeline without provisioning anything and deliberately fails its first attempt, so the repair loop can be exercised offline. Switch PATHFINDER_PROVIDER=zcli once credentials are in place.

The UI is three static pages: index.html is the showcase, run.html starts a run or replays a finished one via ?job=<id>, and about.html is the reference page.

Project Structure

zeroth/
├── zeroth/
│ ├── api/
│ │ ├── main.py # FastAPI app
│ │ └── routers/ # jobs, stream (SSE), bundle, gallery
│ ├── worker/
│ │ ├── main.py # process_analyze / process_verify
│ │ ├── ingest.py # admission control + shallow clone
│ │ ├── fingerprint.py # facts + evidence, no source code
│ │ ├── compatibility.py # deployability verdict (no model call)
│ │ ├── analyze.py # model → manifest JSON
│ │ ├── manifest_schema.py # failure class 1 caught here
│ │ ├── generate.py # Jinja render
│ │ ├── pathfinder.py # real deploy: import, logs, teardown
│ │ ├── repair.py # diagnose → patch → redeploy
│ │ ├── recipes.py # known-good configurations
│ │ └── providers/ # zcli, simulated
│ ├── templates/ # import.yaml.j2, zerops.yaml.j2, deployment.md.j2
│ ├── bus.py # Valkey pub/sub + replay buffer
│ ├── safety.py # host allowlist, size cap, rate limits
│ ├── models.py # jobs, runs, artifacts
│ └── scripts/seed_gallery.py
├── web/ # index.html, run.html, about.html + config.js
├── docs/ # ARCHITECTURE.md, DESIGN.md
├── import.yaml # 5-service project provisioning
└── zerops.yaml # api / worker / web setups

Safety

  • Public GitHub/GitLab only, size-capped, private addresses refused — all enforced at submit time, before a job is created.
  • Repository code never executes on the Zeroth worker; it is only ever built inside the deployment target.
  • Teardown runs in a finally block on every path. A project is kept only when it succeeded and you asked for it to land in your own account.
  • Global concurrency cap and a per-IP hourly limit, keyed on the forwarded client address rather than the platform proxy — otherwise every visitor shares one bucket.
  • No platform secrets are placed in ephemeral project environments.
  • Verification tokens are delete-on-read and never persisted to the database, logs, or the downloadable bundle.

Roadmap

  • More runtimes in the emittable set — Go, Node, PHP beyond the current matrix
  • Diff view between attempts: what the repair loop actually changed
  • Recipe matching before the model call, for repositories with a known shape
  • Re-verify a bundle on demand, to catch platform drift

Built with

Zeroth was developed inside ZCP, Zerops' control-plane container, using Claude as the coding agent driving the zerops_* MCP tooling — provisioning, deploying, reading logs and verifying services against the live project rather than against a local mock. The failure modes documented in zerops.yaml and zeroth/worker/providers/zcli.py were all found that way: on a real account, against a real deployment.

About

From repository to verified deployment — Zerops pathfinder

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages