Skip to main content

Local execution substrate: ship signed, content-addressed data bundles to devices with fail-closed verification and delta sync, then run search and ranking locally through a deterministic router.

Project description

EdgeProc

Ship a big file to a lot of devices — and let every one of them prove it's the real file, unmodified, before using it. Then send only the parts that changed.

CI License: MIT Python 3.13+

The problem, as a story

When a video game ships an update, your console doesn't re-download the whole 80 GB game. It downloads a small patch. And before it installs anything, it checks a signature to confirm the patch really came from the studio — not from someone who slipped a modified file onto a mirror.

Plenty of software that isn't a game needs exactly that and rarely gets it. A search index. A machine-learning model. An offline catalog or price list. These files are big, they change often, and they have to land on phones, browsers, and small boxes you don't own or control.

Which leaves two awkward questions:

  1. Is this actually my file? It crossed someone else's network and sat on someone else's CDN. If it arrived corrupted — or quietly swapped — would you find out? Or would your app keep running and just start giving wrong answers?
  2. Do I have to send all of it again? If one row of a 400 MB index changed, making every user re-download 400 MB is a real bandwidth bill and a bad experience.

EdgeProc is a Python library that answers both. You publish once; any number of devices pull the update, verify it genuinely came from you and arrived intact, and fetch only the bytes that actually changed.

How it answers them

  • Every file is split into chunks, and each chunk is named by a fingerprint of its own contents. That fingerprint (a SHA-256 hash) changes completely if even one byte changes — so a corrupted or tampered chunk no longer matches the name it was requested under, and is refused. The technical name for this is a content-addressed store, or CAS.
  • Exactly one small file is signed, and it vouches for everything else. The publisher signs a version pointer: a few bytes saying "version 1.0.1 is live, and its file list is <hash>". That file list (the manifest) names every chunk by hash. So one signature check covers the entire release, and there's only one secret to protect.
  • Chunk boundaries follow the content, not fixed offsets. Edit one line in the middle of a big file and only the chunk holding that line changes — everything after it keeps its old fingerprint instead of shifting. That's what makes the next update a small delta.
  • If verification fails, nothing is installed. Not "installed with a warning." The sync exits non-zero and the previous good version stays live.

Once the data has landed, EdgeProc also runs the search and ranking on the device — no embedding API, no vector database, no ranking server in the request path.

See it work

Everything below was run to produce the output shown. About five minutes, most of it a one-time model download.

git clone https://github.com/hseshadr/edge-proc.git
cd edge-proc
uv sync --all-extras   # first run also downloads a ~90 MB embedding model

1. Make something worth shipping

A small product catalog, turned into a searchable index on disk.

cat > save_index.py <<'PY'
import asyncio
from pathlib import Path

from edgeproc_core.vector_mgmt.core.types import IndexConfig, VectorEmbedding

from edgeproc.localvec.encoder import TextEncoder
from edgeproc.localvec.faiss_index import FaissVectorIndex

CATALOG = {
    "p1": "red running shoes",
    "p2": "waterproof hiking boots",
    "p3": "blue denim jacket",
    "p4": "trail running sneakers",
}


async def main() -> None:
    ids, texts = list(CATALOG), list(CATALOG.values())
    encoder = TextEncoder()
    index = FaissVectorIndex("catalog_idx", IndexConfig(dimension=encoder.dim))
    await index.insert(
        [
            VectorEmbedding(entity_id=i, embedding=v.tolist())
            for i, v in zip(ids, encoder.encode_texts(texts), strict=True)
        ]
    )
    index.save(Path("catalog_idx"))


asyncio.run(main())
PY

uv run python save_index.py
ls catalog_idx
index.faiss
state.json

2. Mint a signing key

private.key stays on your build machine forever. public.key is what devices are given, and it's the only thing they have to trust.

uv run edgeproc keygen --out keys
wrote keys/private.key and keys/public.key

3. Publish a signed release

mkdir -p src && cp -r catalog_idx src/

uv run edgeproc publish \
    --src src --origin-dir origin \
    --key keys/private.key \
    --bundle-id catalog --version 1.0.0 --pretty
published v1.0.0 manifest=c4c28ab05da5

origin/ is now a plain directory of hash-named files. Put it behind any static web server or CDN as-is — there is no application server to run.

4. Pull it onto a device

uv run edgeproc sync \
    --base-url origin --cache-dir cache \
    --key keys/public.key \
    --materialize-to materialized --pretty
synced v1.0.0 manifest=c4c28ab05da5 chunks_fetched=2 chunks_reused=0 bytes_fetched=5903

5. Search the file that just arrived

cat > task.json <<'JSON'
{"kind": "search", "payload": {"query": "shoes for running", "k": 3}, "privacy_mode": "local_only"}
JSON

uv run edgeproc route --index-dir materialized/catalog_idx --task task.json --pretty
success=True runtime=localvec latency=112.7ms
  p1  0.219
  p4  0.246
  p2  0.556

p1 is "red running shoes" and p4 is "trail running sneakers" — matched by meaning, on the machine, with nothing in the request path but local code. The hashes and distances above reproduce exactly; only latency varies by machine.

6. Now watch it refuse to be fooled

This is the part worth trying yourself, because it's the whole point.

Nothing changed? Then nothing is downloaded.

uv run edgeproc sync --base-url origin --cache-dir cache --key keys/public.key --pretty
synced v1.0.0 manifest=c4c28ab05da5 chunks_fetched=0 chunks_reused=2 bytes_fetched=0

A small edit ships as a small delta. Publish 1.0.1 with one line appended, then re-sync:

echo "tiny edit" >> src/catalog_idx/state.json

uv run edgeproc publish --src src --origin-dir origin --key keys/private.key \
    --bundle-id catalog --version 1.0.1 --pretty
uv run edgeproc sync --base-url origin --cache-dir cache --key keys/public.key \
    --materialize-to materialized --pretty
published v1.0.1 manifest=312b66ae9d63
synced v1.0.1 manifest=312b66ae9d63 chunks_fetched=1 chunks_reused=1 bytes_fetched=157

157 bytes instead of 5,903 — it re-fetched the one chunk that changed and reused the rest.

No key means no sync. There is no "just this once" mode:

uv run edgeproc sync --base-url origin --cache-dir cache2 --pretty; echo "exit=$?"
[config.missing] no trust root: pass --key or set EDGEPROC_TRUST_ROOT_PUBKEY_PATH (refusing to sync)
exit=1

That [config.missing] prefix is a canonical error code, not decoration — every refusal carries one, and EDGEPROC_ERROR_FORMAT=json turns the same refusal into a machine-readable RFC 9457 object so a script can branch on it.

Corrupt a chunk on the server and the device rejects the whole release. Overwrite any file under origin/chunk/ and sync into a fresh cache:

printf 'corrupted' > "origin/chunk/$(ls origin/chunk | head -1)"
uv run edgeproc sync --base-url origin --cache-dir cache3 --key keys/public.key --pretty
echo "exit=$?"
ls cache3
[bundle.integrity_failed] sync failed: stored chunk failed to decompress
exit=1
chunks
manifests

Compare that to the healthy cache/, which has an active directory. cache3/ never got one: the bad version was never promoted, and a device in this state keeps serving the last good version instead of silently serving corrupted data.

Prefer to stay in Python?

The same search, in-process, without the CLI:

import asyncio

from edgeproc import EdgeProc, PrivacyMode, RuntimeRegistry, Task, TaskKind
from edgeproc.localvec.encoder import TextEncoder
from edgeproc.localvec.runtime import LocalVecRuntime

CATALOG = {"p1": "red running shoes", "p2": "waterproof hiking boots", "p3": "trail sneakers"}


async def main() -> None:
    runtime = await LocalVecRuntime.from_texts(CATALOG, encoder=TextEncoder())
    registry = RuntimeRegistry(); registry.register(runtime)
    result = await EdgeProc(registry=registry).run(
        Task(kind=TaskKind.SEARCH, payload={"query": "shoes for running"}, privacy_mode=PrivacyMode.LOCAL_ONLY)
    )
    for entity_id, distance in result.payload["results"]:
        print(f"  {entity_id}  {CATALOG[entity_id]:<24} distance={distance:.3f}")


asyncio.run(main())
  p1  red running shoes        distance=0.219
  p3  trail sneakers           distance=0.374
  p2  waterproof hiking boots  distance=0.556

Swap TaskKind.SEARCH for TaskKind.EMBED (raw vectors) or TaskKind.RANK (keyword + meaning-based ranking combined).

Why you'd reach for this

Moving both the data and the compute to the device buys four things at once:

  • The Nth query costs nothing. Per-request cloud bills for embeddings, vector search, and reranking collapse into a one-time bundle build.
  • Traffic spikes land on clients, not your servers. A launch or a front-page link is absorbed by users' own devices. Nothing to autoscale, nothing to fall over.
  • It survives weak or absent connectivity. After one sync the device needs no network at all to keep answering queries.
  • Tampering is caught, not tolerated. Verification is fail-closed, and because the data is searched locally, it never leaves the device to begin with.

Typical shapes: an app that recommends the next item with no per-search cloud bill however many users you have; a privacy-sensitive tool where user data must never leave the machine; an edge or IoT box that needs fast local search without standing up a backend.


Under the hood (for developers)

Everything above is the friendly surface. Here is what actually happens, in the real terms.

Install

edge-proc isn't on PyPI yet, so the working install is the clone-and-go setup above — one command, no sibling checkouts:

git clone https://github.com/hseshadr/edge-proc.git
cd edge-proc
uv sync --all-extras   # core + extras + dev tooling

That Just Works — edgeproc-core resolves from PyPI (edgeproc-core>=0.2.1, the first release shipping the edgeproc_core import package), so uv sync fetches everything; nothing else to clone. Co-developing edgeproc-core alongside EdgeProc? Clone it next to this repo and add the path override commented in pyproject.toml.

Or install from PyPI — the extras install directly:

pip install edge-proc                    # core + CLI (pure router, contracts)
pip install edge-proc[localvec]          # + FAISS vector runtime (EMBED / SEARCH / RANK)
pip install edge-proc[bundles]           # + manifest + checksum sync substrate
pip install edge-proc[localvec,bundles]  # full local substrate

EdgeProc is purely a dependency — a library an application embeds, not a service you sign up for. The core is tiny; the heavy machinery (FAISS, sync) is opt-in behind extras. It builds on edgeproc-core: the FAISS index here is a concrete implementation of that library's VectorIndex Protocol.

The deterministic router

You hand EdgeProc a Task and a router picks which engine (a "runtime") serves it. That router is a plain rulebook, never an AI — it asks each registered runtime "do you accept this task?" and picks the first that says yes. Because it's a pure function, the same Task against the same runtimes always routes the same way, so a trace is replayable and you can prove which runtime touched a request.

The typed result and the Task/budget model

A Task carries its kind (EMBED / SEARCH / RANK), a payload, a privacy_mode, and a latency/memory budget declaration. EdgeProc admits work through a thread-safe MemoryManager: the sum of declared in-flight reservations cannot exceed max_in_flight_memory_mb, and every reservation releases in a finally-safe context. This is deterministic admission control, not a native-RSS limit: the budget remains a declaration, not an enforcement boundary for allocations inside FAISS, NumPy, or another native runtime. The host or container owns RSS, CPU, and process-level termination. Share one MemoryManager across facades when they share a process.

Every run returns a typed ResultEnvelope — a structured object with success, the serving runtime, latency, and the payload — not a loose dict. Typed in, typed out.

The verification chain, precisely

sync verifies the pointer signature against the pinned trust-root pubkey before trusting anything, diffs the manifest against the local cache, fetches only missing chunks, re-checks every chunk against its content address, and only then atomically promotes the new version. A tampered chunk fails its content-address check; a forged pointer fails its signature check — both exit non-zero with no traceback, and neither promotes into the cache.

Chunking is content-defined (GearCDC) and chunks are zstd-compressed. Add --http to sync, serving origin/ over any static HTTP server or CDN, to go over the wire instead of the filesystem; the contract is identical and only the transport changes.

Configuration: EdgeProcSettings + EDGEPROC_-prefixed env vars

Deploy-time config is read lazily from the environment / .env via EdgeProcSettings (edgeproc/core/settings.py). It validates documented settings but ignores unrelated host variables, so an embedded library coexists with the application's own environment. Env vars use the EDGEPROC_ prefix (except the HF token, which uses the ecosystem-standard HF_TOKEN):

Setting Env var Default Purpose
model_name EDGEPROC_MODEL_NAME sentence-transformers/all-MiniLM-L6-v2 Embedding model.
hf_token HF_TOKEN None Hugging Face auth token.
default_k EDGEPROC_DEFAULT_K 10 Default top-k results.
http_timeout EDGEPROC_HTTP_TIMEOUT 30.0 Bundle HTTP fetch timeout (s).
mutation_lock_timeout EDGEPROC_MUTATION_LOCK_TIMEOUT 30.0 Bounded cross-process publish/sync/promote/GC lock wait (s).
task_budget_ms EDGEPROC_TASK_BUDGET_MS 5000 Default per-task latency budget.
task_budget_memory_mb EDGEPROC_TASK_BUDGET_MEMORY_MB 256 Default per-task memory budget.
max_in_flight_memory_mb EDGEPROC_MAX_IN_FLIGHT_MEMORY_MB 512 Sum of declared task reservations admitted concurrently by one EdgeProc instance.
max_materialize_bytes EDGEPROC_MAX_MATERIALIZE_BYTES 256 MiB Maximum one file materialized into a returned bytes value.
max_decompressed_bytes EDGEPROC_MAX_DECOMPRESSED_BYTES 64 MiB Ceiling on one chunk's decompressed plaintext — refuses a zstd bomb before it exhausts memory. A legitimate chunk is ≤256 KiB, so this never rejects real data.
max_fetch_bytes EDGEPROC_MAX_FETCH_BYTES 256 MiB Ceiling on a single HTTP fetch body (pointer, manifest, or chunk) — bounds a hostile origin.
max_sync_total_bytes EDGEPROC_MAX_SYNC_TOTAL_BYTES 4 GiB Aggregate bytes one sync will pull before refusing — disk-exhaustion defense against a runaway manifest.
max_sync_files EDGEPROC_MAX_SYNC_FILES 100000 Aggregate file count one sync will pull before refusing, for the same reason.
rrf_k_window EDGEPROC_RRF_K_WINDOW 60 RRF rank-window constant for hybrid fusion.
trust_root_pubkey_path EDGEPROC_TRUST_ROOT_PUBKEY_PATH None Pinned sync trust-root pubkey (no key ⇒ sync refused).

That is the complete set — all 15 fields of EdgeProcSettings. A test asserts this table matches the settings object field-for-field, so a new setting cannot ship undocumented.

One more environment variable exists that is deliberately not an EdgeProcSettings field, because it steers CLI output rather than library behavior:

Env var Default Purpose
EDGEPROC_ERROR_FORMAT text Set to json and every fail-closed exit prints one RFC 9457 Problem Details object on stderr instead of a text line — so a CI step or supervising process branches on type rather than pattern-matching prose.
$ EDGEPROC_ERROR_FORMAT=json edgeproc sync --base-url ./origin --cache-dir ./cache --key absent.key
{"detail": "could not read trust-root key absent.key: [Errno 2] No such file or directory: 'absent.key'", "field": "--key", "title": "A required setting is missing: --key.", "type": "config.missing"}

It is read straight from the environment rather than through EdgeProcSettings on purpose: this runs on the failure path, and building the settings object there would let an unrelated malformed variable raise while reporting another error, swallowing the refusal being reported.

For the full picture — system context, bundle lifecycle, the verification chain, and the module map — see docs/ARCHITECTURE.md (with d2 diagrams).

Measured numbers

uv sync --all-extras
uv run poe gate
uv run python benchmarks/benchmark.py

Both commands check themselves. The benchmark runs a fixed, offline fixture and prints JSON with the latency it measured, peak memory, the machine it ran on, and pass/fail against each budget — so you get your numbers rather than having to trust someone else's.

The figures measured here, and the hardware they were measured on, are recorded in docs/OPERATIONS.md. That is the one place they live: this README deliberately does not restate them, because a number copied into two documents is a number that will eventually disagree with itself.

Status & roadmap

Shipped (v0): a deterministic non-AI router, a FAISS-backed local-vector runtime (EMBED / SEARCH / RANK), and a content-addressed, signed-bundle sync substrate (pinned ed25519 + content-defined chunking), all behind opt-in extras.

Roadmap — not built yet (kept as Protocol seams, not in v0):

  • First-party WASM kernel v0 — one deterministic hot path (chunk hash/verify, BM25, or rerank math), Rust→wasm32, running identically in the browser and in Python via wasmtime, filling the CUSTOM_WASM seam — roadmap, not built yet; full definition of done in ROADMAP.md.
  • Biscuit capability tokens for fine-grained, attenuable authorization — roadmap, not built yet.
  • Sigstore keyless bundle signing as an alternative to pinned ed25519 keys — roadmap, not built yet.

Docs

  • docs/QUICKSTART.md — the keygen → publish → sync → route loop as a standalone five-minute walkthrough.
  • docs/ARCHITECTURE.md — system context, bundle lifecycle, CAS + manifest, module boundaries, seams.
  • docs/OPERATIONS.md — threat model, privacy flow, recovery/SLA ownership, resource ceilings, and the measured performance gate.
  • docs/diagrams/ — d2 sources + rendered SVGs.

Develop

uv sync --all-extras   # core + extras + dev tooling
uv run poe gate        # lint + format-check + mypy strict + Radon Grade A + pytest (≥90% statement+branch cov)

poe gate mirrors CI exactly — if it passes locally, CI passes.

About

EdgeProc — also written edge-proc and edgeproc; canonical repo hseshadr/edge-proc — is the open-source, local-first delivery-and-search substrate described above. It builds on edgeproc-core, the vector-partitioning protocol its FAISS runtime implements. Canonical entity page: edge-reco.com/edgeproc, on a domain we control. It is not affiliated with any other product or company named "EdgeProc".

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

edge_proc-0.1.5.tar.gz (98.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

edge_proc-0.1.5-py3-none-any.whl (62.0 kB view details)

Uploaded Python 3

File details

Details for the file edge_proc-0.1.5.tar.gz.

File metadata

  • Download URL: edge_proc-0.1.5.tar.gz
  • Upload date:
  • Size: 98.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.4

File hashes

Hashes for edge_proc-0.1.5.tar.gz
Algorithm Hash digest
SHA256 b06ac5654b194f32d427cc53e333fe06f40c9613a4ab9fe9fbc7acc0cdd08bf3
MD5 7a59d5e9e2817c1e2453c0371ab9dfc8
BLAKE2b-256 f97431cd6918c7fabe84db7aa00aa512909a25213e3ee90d1593f36142f2f2e7

See more details on using hashes here.

File details

Details for the file edge_proc-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: edge_proc-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 62.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.4

File hashes

Hashes for edge_proc-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 052371757f084e2c03b5fdaadbf5ad6958dcbc0db29803aeb7a995ce4b3895d3
MD5 790a98e744ad1c12b35a68be55583aca
BLAKE2b-256 5dad0db6fbf228b3f03fe5c4dfd72f6a2efbbb0084e56feb121c40e11eb43d69

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page