edge-proc 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edge_proc-0.1.5.dist-info/METADATA +512 -0
- edge_proc-0.1.5.dist-info/RECORD +36 -0
- edge_proc-0.1.5.dist-info/WHEEL +4 -0
- edge_proc-0.1.5.dist-info/entry_points.txt +2 -0
- edge_proc-0.1.5.dist-info/licenses/LICENSE +21 -0
- edgeproc/__init__.py +47 -0
- edgeproc/_version.py +3 -0
- edgeproc/bundles/__init__.py +0 -0
- edgeproc/bundles/adapters.py +101 -0
- edgeproc/bundles/cas.py +346 -0
- edgeproc/bundles/chunking.py +99 -0
- edgeproc/bundles/containment.py +56 -0
- edgeproc/bundles/manifest.py +147 -0
- edgeproc/bundles/publish.py +174 -0
- edgeproc/bundles/signing.py +102 -0
- edgeproc/bundles/sync.py +263 -0
- edgeproc/cli/__init__.py +7 -0
- edgeproc/cli/app.py +551 -0
- edgeproc/core/__init__.py +0 -0
- edgeproc/core/facade.py +119 -0
- edgeproc/core/memory.py +70 -0
- edgeproc/core/models.py +113 -0
- edgeproc/core/protocols.py +53 -0
- edgeproc/core/registry.py +23 -0
- edgeproc/core/router.py +24 -0
- edgeproc/core/settings.py +73 -0
- edgeproc/core/telemetry.py +40 -0
- edgeproc/errors.py +82 -0
- edgeproc/localvec/__init__.py +0 -0
- edgeproc/localvec/encoder.py +82 -0
- edgeproc/localvec/faiss_index.py +232 -0
- edgeproc/localvec/fusion.py +33 -0
- edgeproc/localvec/loader.py +33 -0
- edgeproc/localvec/runtime.py +171 -0
- edgeproc/localvec/searcher.py +43 -0
- edgeproc/py.typed +0 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edge-proc
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: 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.
|
|
5
|
+
Project-URL: Homepage, https://github.com/hseshadr/edge-proc
|
|
6
|
+
Project-URL: Repository, https://github.com/hseshadr/edge-proc
|
|
7
|
+
Project-URL: Issues, https://github.com/hseshadr/edge-proc/issues
|
|
8
|
+
Author-email: Harish Seshadri <harish.seshadri@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Requires-Dist: edgeproc-core>=0.2.1
|
|
19
|
+
Requires-Dist: pydantic-settings>=2.5
|
|
20
|
+
Requires-Dist: pydantic>=2.9
|
|
21
|
+
Requires-Dist: typer>=0.26.8
|
|
22
|
+
Provides-Extra: bundles
|
|
23
|
+
Requires-Dist: cryptography>=44; extra == 'bundles'
|
|
24
|
+
Requires-Dist: filelock>=3.20; extra == 'bundles'
|
|
25
|
+
Requires-Dist: httpx>=0.27; extra == 'bundles'
|
|
26
|
+
Requires-Dist: packaging>=24; extra == 'bundles'
|
|
27
|
+
Requires-Dist: structlog>=26.1.0; extra == 'bundles'
|
|
28
|
+
Requires-Dist: zstandard>=0.25; extra == 'bundles'
|
|
29
|
+
Provides-Extra: localvec
|
|
30
|
+
Requires-Dist: faiss-cpu>=1.14.3; extra == 'localvec'
|
|
31
|
+
Requires-Dist: numpy>=2.5.1; extra == 'localvec'
|
|
32
|
+
Requires-Dist: rank-bm25>=0.2; extra == 'localvec'
|
|
33
|
+
Requires-Dist: sentence-transformers>=5.6.0; extra == 'localvec'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# EdgeProc
|
|
37
|
+
|
|
38
|
+
**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.**
|
|
39
|
+
|
|
40
|
+
[](https://github.com/hseshadr/edge-proc/actions/workflows/ci.yml)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
[](https://www.python.org/downloads/)
|
|
43
|
+
|
|
44
|
+
## The problem, as a story
|
|
45
|
+
|
|
46
|
+
When a video game ships an update, your console doesn't re-download the whole 80 GB game. It
|
|
47
|
+
downloads a small patch. And before it installs anything, it checks a signature to confirm the
|
|
48
|
+
patch really came from the studio — not from someone who slipped a modified file onto a mirror.
|
|
49
|
+
|
|
50
|
+
Plenty of software that isn't a game needs exactly that and rarely gets it. A search index. A
|
|
51
|
+
machine-learning model. An offline catalog or price list. These files are big, they change
|
|
52
|
+
often, and they have to land on phones, browsers, and small boxes you don't own or control.
|
|
53
|
+
|
|
54
|
+
Which leaves two awkward questions:
|
|
55
|
+
|
|
56
|
+
1. **Is this actually my file?** It crossed someone else's network and sat on someone else's
|
|
57
|
+
CDN. If it arrived corrupted — or quietly swapped — would you find out? Or would your app
|
|
58
|
+
keep running and just start giving wrong answers?
|
|
59
|
+
2. **Do I have to send all of it again?** If one row of a 400 MB index changed, making every
|
|
60
|
+
user re-download 400 MB is a real bandwidth bill and a bad experience.
|
|
61
|
+
|
|
62
|
+
**EdgeProc is a Python library that answers both.** You publish once; any number of devices
|
|
63
|
+
pull the update, verify it genuinely came from you and arrived intact, and fetch only the
|
|
64
|
+
bytes that actually changed.
|
|
65
|
+
|
|
66
|
+
## How it answers them
|
|
67
|
+
|
|
68
|
+
- **Every file is split into chunks, and each chunk is named by a fingerprint of its own
|
|
69
|
+
contents.** That fingerprint (a SHA-256 hash) changes completely if even one byte changes —
|
|
70
|
+
so a corrupted or tampered chunk no longer matches the name it was requested under, and is
|
|
71
|
+
refused. The technical name for this is a *content-addressed store*, or CAS.
|
|
72
|
+
- **Exactly one small file is signed, and it vouches for everything else.** The publisher signs
|
|
73
|
+
a *version pointer*: a few bytes saying "version 1.0.1 is live, and its file list is
|
|
74
|
+
`<hash>`". That file list (the *manifest*) names every chunk by hash. So one signature check
|
|
75
|
+
covers the entire release, and there's only one secret to protect.
|
|
76
|
+
- **Chunk boundaries follow the content, not fixed offsets.** Edit one line in the middle of a
|
|
77
|
+
big file and only the chunk holding that line changes — everything after it keeps its old
|
|
78
|
+
fingerprint instead of shifting. That's what makes the next update a small delta.
|
|
79
|
+
- **If verification fails, nothing is installed.** Not "installed with a warning." The sync
|
|
80
|
+
exits non-zero and the previous good version stays live.
|
|
81
|
+
|
|
82
|
+
Once the data has landed, EdgeProc also runs the search and ranking **on the device** — no
|
|
83
|
+
embedding API, no vector database, no ranking server in the request path.
|
|
84
|
+
|
|
85
|
+
## See it work
|
|
86
|
+
|
|
87
|
+
Everything below was run to produce the output shown. About five minutes, most of it a
|
|
88
|
+
one-time model download.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git clone https://github.com/hseshadr/edge-proc.git
|
|
92
|
+
cd edge-proc
|
|
93
|
+
uv sync --all-extras # first run also downloads a ~90 MB embedding model
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 1. Make something worth shipping
|
|
97
|
+
|
|
98
|
+
A small product catalog, turned into a searchable index on disk.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cat > save_index.py <<'PY'
|
|
102
|
+
import asyncio
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
from edgeproc_core.vector_mgmt.core.types import IndexConfig, VectorEmbedding
|
|
106
|
+
|
|
107
|
+
from edgeproc.localvec.encoder import TextEncoder
|
|
108
|
+
from edgeproc.localvec.faiss_index import FaissVectorIndex
|
|
109
|
+
|
|
110
|
+
CATALOG = {
|
|
111
|
+
"p1": "red running shoes",
|
|
112
|
+
"p2": "waterproof hiking boots",
|
|
113
|
+
"p3": "blue denim jacket",
|
|
114
|
+
"p4": "trail running sneakers",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def main() -> None:
|
|
119
|
+
ids, texts = list(CATALOG), list(CATALOG.values())
|
|
120
|
+
encoder = TextEncoder()
|
|
121
|
+
index = FaissVectorIndex("catalog_idx", IndexConfig(dimension=encoder.dim))
|
|
122
|
+
await index.insert(
|
|
123
|
+
[
|
|
124
|
+
VectorEmbedding(entity_id=i, embedding=v.tolist())
|
|
125
|
+
for i, v in zip(ids, encoder.encode_texts(texts), strict=True)
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
index.save(Path("catalog_idx"))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
asyncio.run(main())
|
|
132
|
+
PY
|
|
133
|
+
|
|
134
|
+
uv run python save_index.py
|
|
135
|
+
ls catalog_idx
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
index.faiss
|
|
140
|
+
state.json
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 2. Mint a signing key
|
|
144
|
+
|
|
145
|
+
`private.key` stays on your build machine forever. `public.key` is what devices are given, and
|
|
146
|
+
it's the only thing they have to trust.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
uv run edgeproc keygen --out keys
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
wrote keys/private.key and keys/public.key
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 3. Publish a signed release
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
mkdir -p src && cp -r catalog_idx src/
|
|
160
|
+
|
|
161
|
+
uv run edgeproc publish \
|
|
162
|
+
--src src --origin-dir origin \
|
|
163
|
+
--key keys/private.key \
|
|
164
|
+
--bundle-id catalog --version 1.0.0 --pretty
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
published v1.0.0 manifest=c4c28ab05da5
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`origin/` is now a plain directory of hash-named files. Put it behind any static web server or
|
|
172
|
+
CDN as-is — there is no application server to run.
|
|
173
|
+
|
|
174
|
+
### 4. Pull it onto a device
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv run edgeproc sync \
|
|
178
|
+
--base-url origin --cache-dir cache \
|
|
179
|
+
--key keys/public.key \
|
|
180
|
+
--materialize-to materialized --pretty
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
synced v1.0.0 manifest=c4c28ab05da5 chunks_fetched=2 chunks_reused=0 bytes_fetched=5903
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 5. Search the file that just arrived
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
cat > task.json <<'JSON'
|
|
191
|
+
{"kind": "search", "payload": {"query": "shoes for running", "k": 3}, "privacy_mode": "local_only"}
|
|
192
|
+
JSON
|
|
193
|
+
|
|
194
|
+
uv run edgeproc route --index-dir materialized/catalog_idx --task task.json --pretty
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
success=True runtime=localvec latency=112.7ms
|
|
199
|
+
p1 0.219
|
|
200
|
+
p4 0.246
|
|
201
|
+
p2 0.556
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`p1` is "red running shoes" and `p4` is "trail running sneakers" — matched by meaning, on the
|
|
205
|
+
machine, with nothing in the request path but local code. The hashes and distances above
|
|
206
|
+
reproduce exactly; only `latency` varies by machine.
|
|
207
|
+
|
|
208
|
+
### 6. Now watch it refuse to be fooled
|
|
209
|
+
|
|
210
|
+
This is the part worth trying yourself, because it's the whole point.
|
|
211
|
+
|
|
212
|
+
**Nothing changed? Then nothing is downloaded.**
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
uv run edgeproc sync --base-url origin --cache-dir cache --key keys/public.key --pretty
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
synced v1.0.0 manifest=c4c28ab05da5 chunks_fetched=0 chunks_reused=2 bytes_fetched=0
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**A small edit ships as a small delta.** Publish `1.0.1` with one line appended, then re-sync:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
echo "tiny edit" >> src/catalog_idx/state.json
|
|
226
|
+
|
|
227
|
+
uv run edgeproc publish --src src --origin-dir origin --key keys/private.key \
|
|
228
|
+
--bundle-id catalog --version 1.0.1 --pretty
|
|
229
|
+
uv run edgeproc sync --base-url origin --cache-dir cache --key keys/public.key \
|
|
230
|
+
--materialize-to materialized --pretty
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
published v1.0.1 manifest=312b66ae9d63
|
|
235
|
+
synced v1.0.1 manifest=312b66ae9d63 chunks_fetched=1 chunks_reused=1 bytes_fetched=157
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
157 bytes instead of 5,903 — it re-fetched the one chunk that changed and reused the rest.
|
|
239
|
+
|
|
240
|
+
**No key means no sync.** There is no "just this once" mode:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
uv run edgeproc sync --base-url origin --cache-dir cache2 --pretty; echo "exit=$?"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
[config.missing] no trust root: pass --key or set EDGEPROC_TRUST_ROOT_PUBKEY_PATH (refusing to sync)
|
|
248
|
+
exit=1
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
That `[config.missing]` prefix is a canonical error code, not decoration — every refusal
|
|
252
|
+
carries one, and `EDGEPROC_ERROR_FORMAT=json` turns the same refusal into a machine-readable
|
|
253
|
+
[RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) object so a script can branch on it.
|
|
254
|
+
|
|
255
|
+
**Corrupt a chunk on the server and the device rejects the whole release.** Overwrite any file
|
|
256
|
+
under `origin/chunk/` and sync into a fresh cache:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
printf 'corrupted' > "origin/chunk/$(ls origin/chunk | head -1)"
|
|
260
|
+
uv run edgeproc sync --base-url origin --cache-dir cache3 --key keys/public.key --pretty
|
|
261
|
+
echo "exit=$?"
|
|
262
|
+
ls cache3
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
[bundle.integrity_failed] sync failed: stored chunk failed to decompress
|
|
267
|
+
exit=1
|
|
268
|
+
chunks
|
|
269
|
+
manifests
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Compare that to the healthy `cache/`, which has an `active` directory. `cache3/` never got one:
|
|
273
|
+
the bad version was never promoted, and a device in this state keeps serving the last good
|
|
274
|
+
version instead of silently serving corrupted data.
|
|
275
|
+
|
|
276
|
+
### Prefer to stay in Python?
|
|
277
|
+
|
|
278
|
+
The same search, in-process, without the CLI:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
import asyncio
|
|
282
|
+
|
|
283
|
+
from edgeproc import EdgeProc, PrivacyMode, RuntimeRegistry, Task, TaskKind
|
|
284
|
+
from edgeproc.localvec.encoder import TextEncoder
|
|
285
|
+
from edgeproc.localvec.runtime import LocalVecRuntime
|
|
286
|
+
|
|
287
|
+
CATALOG = {"p1": "red running shoes", "p2": "waterproof hiking boots", "p3": "trail sneakers"}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def main() -> None:
|
|
291
|
+
runtime = await LocalVecRuntime.from_texts(CATALOG, encoder=TextEncoder())
|
|
292
|
+
registry = RuntimeRegistry(); registry.register(runtime)
|
|
293
|
+
result = await EdgeProc(registry=registry).run(
|
|
294
|
+
Task(kind=TaskKind.SEARCH, payload={"query": "shoes for running"}, privacy_mode=PrivacyMode.LOCAL_ONLY)
|
|
295
|
+
)
|
|
296
|
+
for entity_id, distance in result.payload["results"]:
|
|
297
|
+
print(f" {entity_id} {CATALOG[entity_id]:<24} distance={distance:.3f}")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
asyncio.run(main())
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```text
|
|
304
|
+
p1 red running shoes distance=0.219
|
|
305
|
+
p3 trail sneakers distance=0.374
|
|
306
|
+
p2 waterproof hiking boots distance=0.556
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Swap `TaskKind.SEARCH` for `TaskKind.EMBED` (raw vectors) or `TaskKind.RANK` (keyword +
|
|
310
|
+
meaning-based ranking combined).
|
|
311
|
+
|
|
312
|
+
## Why you'd reach for this
|
|
313
|
+
|
|
314
|
+
Moving both the data and the compute to the device buys four things at once:
|
|
315
|
+
|
|
316
|
+
- **The Nth query costs nothing.** Per-request cloud bills for embeddings, vector search, and
|
|
317
|
+
reranking collapse into a one-time bundle build.
|
|
318
|
+
- **Traffic spikes land on clients, not your servers.** A launch or a front-page link is
|
|
319
|
+
absorbed by users' own devices. Nothing to autoscale, nothing to fall over.
|
|
320
|
+
- **It survives weak or absent connectivity.** After one sync the device needs no network at
|
|
321
|
+
all to keep answering queries.
|
|
322
|
+
- **Tampering is caught, not tolerated.** Verification is fail-closed, and because the data is
|
|
323
|
+
searched locally, it never leaves the device to begin with.
|
|
324
|
+
|
|
325
|
+
Typical shapes: an app that recommends the next item with no per-search cloud bill however many
|
|
326
|
+
users you have; a privacy-sensitive tool where user data must never leave the machine; an edge
|
|
327
|
+
or IoT box that needs fast local search without standing up a backend.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Under the hood (for developers)
|
|
332
|
+
|
|
333
|
+
Everything above is the friendly surface. Here is what actually happens, in the real terms.
|
|
334
|
+
|
|
335
|
+
### Install
|
|
336
|
+
|
|
337
|
+
edge-proc isn't on PyPI yet, so the working install is the clone-and-go setup above — one
|
|
338
|
+
command, no sibling checkouts:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
git clone https://github.com/hseshadr/edge-proc.git
|
|
342
|
+
cd edge-proc
|
|
343
|
+
uv sync --all-extras # core + extras + dev tooling
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
That Just Works — `edgeproc-core` resolves from
|
|
347
|
+
[PyPI](https://pypi.org/project/edgeproc-core/) (`edgeproc-core>=0.2.1`, the first
|
|
348
|
+
release shipping the `edgeproc_core` import package), so `uv sync` fetches
|
|
349
|
+
everything; nothing else to clone. Co-developing `edgeproc-core` alongside
|
|
350
|
+
EdgeProc? Clone it next to this repo and add the path override commented in
|
|
351
|
+
`pyproject.toml`.
|
|
352
|
+
|
|
353
|
+
Or install from PyPI — the extras install directly:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
pip install edge-proc # core + CLI (pure router, contracts)
|
|
357
|
+
pip install edge-proc[localvec] # + FAISS vector runtime (EMBED / SEARCH / RANK)
|
|
358
|
+
pip install edge-proc[bundles] # + manifest + checksum sync substrate
|
|
359
|
+
pip install edge-proc[localvec,bundles] # full local substrate
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
EdgeProc is **purely a dependency** — a library an application embeds, not a service you sign
|
|
363
|
+
up for. The core is tiny; the heavy machinery (FAISS, sync) is opt-in behind extras. It builds
|
|
364
|
+
on [`edgeproc-core`](https://github.com/hseshadr/edgeproc-core): the FAISS index here
|
|
365
|
+
is a concrete implementation of that library's `VectorIndex` Protocol.
|
|
366
|
+
|
|
367
|
+
### The deterministic router
|
|
368
|
+
|
|
369
|
+
You hand EdgeProc a `Task` and a router picks which engine (a "runtime") serves it. **That
|
|
370
|
+
router is a plain rulebook, never an AI** — it asks each registered runtime "do you accept this
|
|
371
|
+
task?" and picks the first that says yes. Because it's a pure function, the same `Task` against
|
|
372
|
+
the same runtimes always routes the same way, so a trace is replayable and you can prove which
|
|
373
|
+
runtime touched a request.
|
|
374
|
+
|
|
375
|
+
### The typed result and the Task/budget model
|
|
376
|
+
|
|
377
|
+
A `Task` carries its `kind` (`EMBED` / `SEARCH` / `RANK`), a `payload`, a `privacy_mode`, and a
|
|
378
|
+
latency/memory **budget declaration**. `EdgeProc` admits work through a thread-safe
|
|
379
|
+
`MemoryManager`: the sum of declared in-flight reservations cannot exceed
|
|
380
|
+
`max_in_flight_memory_mb`, and every reservation releases in a `finally`-safe context. This is
|
|
381
|
+
deterministic admission control, **not** a native-RSS limit: the budget remains a declaration,
|
|
382
|
+
not an enforcement boundary for allocations inside FAISS, NumPy, or another native runtime. The
|
|
383
|
+
host or container owns RSS, CPU, and process-level termination. Share one `MemoryManager` across
|
|
384
|
+
facades when they share a process.
|
|
385
|
+
|
|
386
|
+
Every run returns a typed `ResultEnvelope` — a structured object with `success`, the serving
|
|
387
|
+
`runtime`, `latency`, and the `payload` — not a loose dict. Typed in, typed out.
|
|
388
|
+
|
|
389
|
+
### The verification chain, precisely
|
|
390
|
+
|
|
391
|
+
`sync` verifies the pointer signature against the pinned trust-root pubkey **before trusting
|
|
392
|
+
anything**, diffs the manifest against the local cache, fetches only missing chunks, re-checks
|
|
393
|
+
every chunk against its content address, and only then atomically promotes the new version. A
|
|
394
|
+
tampered chunk fails its content-address check; a forged pointer fails its signature check —
|
|
395
|
+
both exit non-zero with no traceback, and neither promotes into the cache.
|
|
396
|
+
|
|
397
|
+
Chunking is content-defined (GearCDC) and chunks are zstd-compressed. Add `--http` to `sync`,
|
|
398
|
+
serving `origin/` over any static HTTP server or CDN, to go over the wire instead of the
|
|
399
|
+
filesystem; the contract is identical and only the transport changes.
|
|
400
|
+
|
|
401
|
+
### Configuration: `EdgeProcSettings` + `EDGEPROC_`-prefixed env vars
|
|
402
|
+
|
|
403
|
+
Deploy-time config is read lazily from the environment / `.env` via `EdgeProcSettings`
|
|
404
|
+
(`edgeproc/core/settings.py`). It validates documented settings but
|
|
405
|
+
ignores unrelated host variables, so an embedded library coexists with the application's own
|
|
406
|
+
environment. Env vars use the `EDGEPROC_` prefix (except the HF token, which uses the
|
|
407
|
+
ecosystem-standard `HF_TOKEN`):
|
|
408
|
+
|
|
409
|
+
| Setting | Env var | Default | Purpose |
|
|
410
|
+
| --- | --- | --- | --- |
|
|
411
|
+
| `model_name` | `EDGEPROC_MODEL_NAME` | `sentence-transformers/all-MiniLM-L6-v2` | Embedding model. |
|
|
412
|
+
| `hf_token` | `HF_TOKEN` | `None` | Hugging Face auth token. |
|
|
413
|
+
| `default_k` | `EDGEPROC_DEFAULT_K` | `10` | Default top-k results. |
|
|
414
|
+
| `http_timeout` | `EDGEPROC_HTTP_TIMEOUT` | `30.0` | Bundle HTTP fetch timeout (s). |
|
|
415
|
+
| `mutation_lock_timeout` | `EDGEPROC_MUTATION_LOCK_TIMEOUT` | `30.0` | Bounded cross-process publish/sync/promote/GC lock wait (s). |
|
|
416
|
+
| `task_budget_ms` | `EDGEPROC_TASK_BUDGET_MS` | `5000` | Default per-task latency budget. |
|
|
417
|
+
| `task_budget_memory_mb` | `EDGEPROC_TASK_BUDGET_MEMORY_MB` | `256` | Default per-task memory budget. |
|
|
418
|
+
| `max_in_flight_memory_mb` | `EDGEPROC_MAX_IN_FLIGHT_MEMORY_MB` | `512` | Sum of declared task reservations admitted concurrently by one `EdgeProc` instance. |
|
|
419
|
+
| `max_materialize_bytes` | `EDGEPROC_MAX_MATERIALIZE_BYTES` | `256 MiB` | Maximum one file materialized into a returned `bytes` value. |
|
|
420
|
+
| `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. |
|
|
421
|
+
| `max_fetch_bytes` | `EDGEPROC_MAX_FETCH_BYTES` | `256 MiB` | Ceiling on a single HTTP fetch body (pointer, manifest, or chunk) — bounds a hostile origin. |
|
|
422
|
+
| `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. |
|
|
423
|
+
| `max_sync_files` | `EDGEPROC_MAX_SYNC_FILES` | `100000` | Aggregate file count one `sync` will pull before refusing, for the same reason. |
|
|
424
|
+
| `rrf_k_window` | `EDGEPROC_RRF_K_WINDOW` | `60` | RRF rank-window constant for hybrid fusion. |
|
|
425
|
+
| `trust_root_pubkey_path` | `EDGEPROC_TRUST_ROOT_PUBKEY_PATH` | `None` | Pinned sync trust-root pubkey (no key ⇒ `sync` refused). |
|
|
426
|
+
|
|
427
|
+
That is the complete set — all 15 fields of `EdgeProcSettings`. A test asserts this table
|
|
428
|
+
matches the settings object field-for-field, so a new setting cannot ship undocumented.
|
|
429
|
+
|
|
430
|
+
One more environment variable exists that is deliberately **not** an `EdgeProcSettings`
|
|
431
|
+
field, because it steers CLI output rather than library behavior:
|
|
432
|
+
|
|
433
|
+
| Env var | Default | Purpose |
|
|
434
|
+
| --- | --- | --- |
|
|
435
|
+
| `EDGEPROC_ERROR_FORMAT` | `text` | Set to `json` and every fail-closed exit prints one [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457) object on stderr instead of a text line — so a CI step or supervising process branches on `type` rather than pattern-matching prose. |
|
|
436
|
+
|
|
437
|
+
```console
|
|
438
|
+
$ EDGEPROC_ERROR_FORMAT=json edgeproc sync --base-url ./origin --cache-dir ./cache --key absent.key
|
|
439
|
+
{"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"}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
It is read straight from the environment rather than through `EdgeProcSettings` on purpose:
|
|
443
|
+
this runs on the failure path, and building the settings object there would let an unrelated
|
|
444
|
+
malformed variable raise *while reporting another error*, swallowing the refusal being reported.
|
|
445
|
+
|
|
446
|
+
For the full picture — system context, bundle lifecycle, the verification chain, and the module
|
|
447
|
+
map — see [**docs/ARCHITECTURE.md**](docs/ARCHITECTURE.md) (with d2 diagrams).
|
|
448
|
+
|
|
449
|
+
### Measured numbers
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
uv sync --all-extras
|
|
453
|
+
uv run poe gate
|
|
454
|
+
uv run python benchmarks/benchmark.py
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Both commands check themselves. The benchmark runs a fixed, offline fixture and prints JSON
|
|
458
|
+
with the latency it measured, peak memory, the machine it ran on, and pass/fail against each
|
|
459
|
+
budget — so you get *your* numbers rather than having to trust someone else's.
|
|
460
|
+
|
|
461
|
+
The figures measured here, and the hardware they were measured on, are recorded in
|
|
462
|
+
[**docs/OPERATIONS.md**](docs/OPERATIONS.md#measured-evidence). That is the one place they
|
|
463
|
+
live: this README deliberately does not restate them, because a number copied into two
|
|
464
|
+
documents is a number that will eventually disagree with itself.
|
|
465
|
+
|
|
466
|
+
## Status & roadmap
|
|
467
|
+
|
|
468
|
+
**Shipped (v0):** a deterministic non-AI router, a FAISS-backed local-vector runtime (`EMBED` /
|
|
469
|
+
`SEARCH` / `RANK`), and a content-addressed, signed-bundle sync substrate (pinned ed25519 +
|
|
470
|
+
content-defined chunking), all behind opt-in extras.
|
|
471
|
+
|
|
472
|
+
**Roadmap — not built yet** (kept as Protocol seams, not in v0):
|
|
473
|
+
|
|
474
|
+
- **First-party WASM kernel v0** — one deterministic hot path (chunk hash/verify, BM25, or
|
|
475
|
+
rerank math), Rust→wasm32, running identically in the browser and in Python via wasmtime,
|
|
476
|
+
filling the `CUSTOM_WASM` seam — *roadmap, not built yet; full definition of done in
|
|
477
|
+
[ROADMAP.md](ROADMAP.md).*
|
|
478
|
+
- Biscuit capability tokens for fine-grained, attenuable authorization — *roadmap, not built yet.*
|
|
479
|
+
- Sigstore keyless bundle signing as an alternative to pinned ed25519 keys — *roadmap, not built yet.*
|
|
480
|
+
|
|
481
|
+
## Docs
|
|
482
|
+
|
|
483
|
+
- [docs/QUICKSTART.md](docs/QUICKSTART.md) — the `keygen → publish → sync → route` loop as a
|
|
484
|
+
standalone five-minute walkthrough.
|
|
485
|
+
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — system context, bundle lifecycle, CAS +
|
|
486
|
+
manifest, module boundaries, seams.
|
|
487
|
+
- [docs/OPERATIONS.md](docs/OPERATIONS.md) — threat model, privacy flow, recovery/SLA ownership,
|
|
488
|
+
resource ceilings, and the measured performance gate.
|
|
489
|
+
- [docs/diagrams/](docs/diagrams/) — d2 sources + rendered SVGs.
|
|
490
|
+
|
|
491
|
+
## Develop
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
uv sync --all-extras # core + extras + dev tooling
|
|
495
|
+
uv run poe gate # lint + format-check + mypy strict + Radon Grade A + pytest (≥90% statement+branch cov)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`poe gate` mirrors CI exactly — if it passes locally, CI passes.
|
|
499
|
+
|
|
500
|
+
## About
|
|
501
|
+
|
|
502
|
+
**EdgeProc** — also written `edge-proc` and `edgeproc`; canonical repo
|
|
503
|
+
[`hseshadr/edge-proc`](https://github.com/hseshadr/edge-proc) — is the open-source, local-first
|
|
504
|
+
delivery-and-search substrate described above. It builds on
|
|
505
|
+
[**edgeproc-core**](https://github.com/hseshadr/edgeproc-core), the vector-partitioning
|
|
506
|
+
protocol its FAISS runtime implements. Canonical entity page:
|
|
507
|
+
[edge-reco.com/edgeproc](https://edge-reco.com/edgeproc), on a domain we control. It is **not
|
|
508
|
+
affiliated with any other product or company named "EdgeProc"**.
|
|
509
|
+
|
|
510
|
+
## License
|
|
511
|
+
|
|
512
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
edgeproc/__init__.py,sha256=_s3PVEdWgM6uGvOILIiY8qFi53GhLWSA3VCyu9RAAY4,1370
|
|
2
|
+
edgeproc/_version.py,sha256=JAwQso-mFo9_4Gbeuq9N3CfGWxTSTvNrWNZCsobN2M4,109
|
|
3
|
+
edgeproc/errors.py,sha256=9kYjX6EgrDP_6sfAJe3FALPLfwJIAakVzsUEQZLGej4,3492
|
|
4
|
+
edgeproc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
edgeproc/bundles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
edgeproc/bundles/adapters.py,sha256=u5QKzNF8wqflTR1FiSl5ZTE3QVqhz-RYLceoEmzUOv8,3567
|
|
7
|
+
edgeproc/bundles/cas.py,sha256=ZhyPW2GBBbE_kHa3MtPeNXE_snSKQO3vZKKrHVBUeGI,14546
|
|
8
|
+
edgeproc/bundles/chunking.py,sha256=DWs5Nj7uknbmNJcJTJH1acwKZeTvg2O7nbyG_AxLedw,4301
|
|
9
|
+
edgeproc/bundles/containment.py,sha256=yKQON-llZWVu0KgKRqHKWF2z7CHy0wunuIt0bBHjZZY,2489
|
|
10
|
+
edgeproc/bundles/manifest.py,sha256=SsSMikjbyObHGHxsbMHWGWZDjUsTCip4C8rL1MWTyhM,6060
|
|
11
|
+
edgeproc/bundles/publish.py,sha256=a__QG9XOVQtzaTJ1l1pvdvDMKfz0MfnOQYghojKII9U,6070
|
|
12
|
+
edgeproc/bundles/signing.py,sha256=JF68SgXxxb5QW6jStANxxyXso3p79JrlvnH_7wLwGxU,4023
|
|
13
|
+
edgeproc/bundles/sync.py,sha256=I8CC4Wt7jY1k2ih16vYvXdZbK-fk219bNjD1di5h4jc,10365
|
|
14
|
+
edgeproc/cli/__init__.py,sha256=9AXZqAQZF_ZZCfjeGvOph5MpJMqVsF2quz1ynQyCEl0,174
|
|
15
|
+
edgeproc/cli/app.py,sha256=P1wg1E9dDcsUCS_DPKl4t1lEUxBdKiHrX3IlfNkoSig,23028
|
|
16
|
+
edgeproc/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
edgeproc/core/facade.py,sha256=kyppCKH4ZNjCKAZgYaKCINJvIjKLRXgPlNCwRQ8NcRc,4995
|
|
18
|
+
edgeproc/core/memory.py,sha256=ROAmjA_Rhs85kZcN_MIcxIYJ9ylXB_a0gQabepcHMn0,2641
|
|
19
|
+
edgeproc/core/models.py,sha256=5QylFUiQQoQhkLpnnOsXKra3-JPdeKCWSJFtpWcMkd4,3543
|
|
20
|
+
edgeproc/core/protocols.py,sha256=sut_zf5b2Kiwocugbelhj7ZqIlVE5ke-CchmI_WoBpo,1913
|
|
21
|
+
edgeproc/core/registry.py,sha256=x6CzGbBF2eWwbMvCAXuZ0ItU0GSAGO9ix83mt8vBmis,721
|
|
22
|
+
edgeproc/core/router.py,sha256=4P1rutQeKYCitj6-AgA8DT0zSF3l824oiQrgarQTB2o,840
|
|
23
|
+
edgeproc/core/settings.py,sha256=vof_y8PtdHDyMj38fzSiViSWCmbyHGUcXVx_NrHb3Sk,3753
|
|
24
|
+
edgeproc/core/telemetry.py,sha256=IsO4CaXuas6L_V0hIIyG8rZ53sEV4Hvnur4FZoKaFfA,1233
|
|
25
|
+
edgeproc/localvec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
edgeproc/localvec/encoder.py,sha256=JJQY6RPEKEENNCi2v0kS2YOAwcAjjaqVLrUukg9mbYM,3308
|
|
27
|
+
edgeproc/localvec/faiss_index.py,sha256=7Q5pZDd2NpecOiiMGPgVeaeMV_A4x-oMyeRRq9DpdAc,10170
|
|
28
|
+
edgeproc/localvec/fusion.py,sha256=ldtHVwaqyOA2W5x8-a24PbrHlVWdTmHMKthk8kVKTi0,1290
|
|
29
|
+
edgeproc/localvec/loader.py,sha256=t3fQF2zyUdxa1jXgPepjqY0eW-C8bGf61VoxYmLTBQU,1444
|
|
30
|
+
edgeproc/localvec/runtime.py,sha256=3fe2ojr_LDjaWAEsQ4WQ0_eOTHS0zVw0cHn7rFRcRRI,6236
|
|
31
|
+
edgeproc/localvec/searcher.py,sha256=2EHNyDnmOJyk62da0d8bn5N3QsB8dtgSEnZuRjszhtA,1578
|
|
32
|
+
edge_proc-0.1.5.dist-info/METADATA,sha256=kOctExDVQVANVC59E_amiR910GdfUWZYhfDqgyKKbWk,22325
|
|
33
|
+
edge_proc-0.1.5.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
34
|
+
edge_proc-0.1.5.dist-info/entry_points.txt,sha256=sh8IGYooeSGlEIwhcDP00P2h4qo_qh-ezAh7oU1u_5U,46
|
|
35
|
+
edge_proc-0.1.5.dist-info/licenses/LICENSE,sha256=DKAeGDB_tj9gKaHjTZRtKQTnm6jf2sgCNl5o9H09baE,1072
|
|
36
|
+
edge_proc-0.1.5.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harish Seshadri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
edgeproc/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""EdgeProc — AI-native local execution substrate.
|
|
2
|
+
|
|
3
|
+
The whole core surface is importable from here: the facade, the contract models,
|
|
4
|
+
the Protocol seams, and the concrete defaults (``DefaultRouter``, ``NullSink``,
|
|
5
|
+
``BufferedSink``, ``RuntimeRegistry``) a consumer actually wires together.
|
|
6
|
+
Runtimes (e.g. ``LocalVecRuntime`` under the ``[localvec]`` extra) and the
|
|
7
|
+
bundle substrate (``[bundles]``) live in their own subpackages so the core stays
|
|
8
|
+
dependency-light.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from edgeproc._version import __version__
|
|
14
|
+
from edgeproc.core.facade import EdgeProc
|
|
15
|
+
from edgeproc.core.memory import MemoryBudgetExceededError, MemoryManager
|
|
16
|
+
from edgeproc.core.models import (
|
|
17
|
+
CapabilityVerdict,
|
|
18
|
+
PrivacyMode,
|
|
19
|
+
Provenance,
|
|
20
|
+
ResultEnvelope,
|
|
21
|
+
Task,
|
|
22
|
+
TaskKind,
|
|
23
|
+
)
|
|
24
|
+
from edgeproc.core.protocols import Router, Runtime, TelemetrySink
|
|
25
|
+
from edgeproc.core.registry import RuntimeRegistry
|
|
26
|
+
from edgeproc.core.router import DefaultRouter
|
|
27
|
+
from edgeproc.core.telemetry import BufferedSink, NullSink
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"BufferedSink",
|
|
31
|
+
"CapabilityVerdict",
|
|
32
|
+
"DefaultRouter",
|
|
33
|
+
"EdgeProc",
|
|
34
|
+
"MemoryBudgetExceededError",
|
|
35
|
+
"MemoryManager",
|
|
36
|
+
"NullSink",
|
|
37
|
+
"PrivacyMode",
|
|
38
|
+
"Provenance",
|
|
39
|
+
"ResultEnvelope",
|
|
40
|
+
"Router",
|
|
41
|
+
"Runtime",
|
|
42
|
+
"RuntimeRegistry",
|
|
43
|
+
"Task",
|
|
44
|
+
"TaskKind",
|
|
45
|
+
"TelemetrySink",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|
edgeproc/_version.py
ADDED
|
File without changes
|