tep 0.11.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 01e96933540bed51cfb5ef9b6edd91154c63d60bf2469eece6e1fbfff42815d5
|
|
4
|
+
data.tar.gz: 47005866b1f57933b4724c19eb1e387b2d3f0d9e52003cc0d155795ed522977a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 58cbe7986525f4eedd43a6b2e603dca7545a8b7d46d0b241a175e451bbcb9ee8d29e1de0e7b8d323f8cee75d93f4ce24a1f8344593affad0534c71f182f7f442
|
|
7
|
+
data.tar.gz: 0f31fe5463c71bef2ee00a1507ff2b9a4c45c49023810e3e2a206e10737b6f794bf89c2b09d1eebf3c23b05321f143bc652b117cf4493e4bf259c2f14bf3861e
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ori Pekelman
|
|
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.
|
data/Makefile
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
TEP_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
|
2
|
+
LIB_DIR := $(TEP_ROOT)/lib/tep
|
|
3
|
+
SPINEL ?= spinel
|
|
4
|
+
TEP := $(TEP_ROOT)/bin/tep
|
|
5
|
+
|
|
6
|
+
# Exported so bin/tep injects the right path into the @TEP_SPHTTP_O@
|
|
7
|
+
# / @TEP_SQLITE_O@ / @TEP_PG_O@ placeholders inside net.rb / sqlite.rb /
|
|
8
|
+
# pg.rb. Override either env var to point at a pre-built .o (useful
|
|
9
|
+
# when the source tree isn't writable). Crypto symbols (sp_crypto_*)
|
|
10
|
+
# live in spinel's libspinel_rt.a since matz/spinel#514, no separate
|
|
11
|
+
# .o needed.
|
|
12
|
+
export TEP_SPHTTP_O := $(LIB_DIR)/sphttp.o
|
|
13
|
+
export TEP_SQLITE_O := $(LIB_DIR)/tep_sqlite.o
|
|
14
|
+
export TEP_PG_O := $(LIB_DIR)/tep_pg.o
|
|
15
|
+
export SPINEL
|
|
16
|
+
|
|
17
|
+
# libpq cflags / libs. pkg-config is the preferred path (libpq ships
|
|
18
|
+
# a .pc file on Linux + Homebrew); pg_config is the fallback for
|
|
19
|
+
# stripped-down hosts.
|
|
20
|
+
#
|
|
21
|
+
# NB: `pg_config --cflags` returns the cflags PostgreSQL itself was
|
|
22
|
+
# built with (warning flags, -O2, ...), NOT the cflags a libpq
|
|
23
|
+
# CONSUMER needs. We want -I<includedir>; the consumer cflags are
|
|
24
|
+
# constructed from `pg_config --includedir`. Same for -L from
|
|
25
|
+
# `--libdir`. If neither lookup finds libpq, both vars stay empty
|
|
26
|
+
# and the compile fails at link time with "cannot find -lpq" --
|
|
27
|
+
# the right loud failure for "you wanted Tep::PG but didn't install
|
|
28
|
+
# libpq".
|
|
29
|
+
TEP_PG_CFLAGS ?= $(shell \
|
|
30
|
+
pkg-config --cflags libpq 2>/dev/null || \
|
|
31
|
+
pg_config --includedir 2>/dev/null | sed -e 's|^|-I|')
|
|
32
|
+
TEP_PG_LIBS ?= $(shell \
|
|
33
|
+
pkg-config --libs libpq 2>/dev/null || \
|
|
34
|
+
(pg_config --libdir 2>/dev/null | sed -e 's|^|-L|' ; echo "-lpq") | tr '\n' ' ')
|
|
35
|
+
export TEP_PG_CFLAGS TEP_PG_LIBS
|
|
36
|
+
|
|
37
|
+
.PHONY: all clean helper hello sinatra_style bench bench-tep bench-sinatra demo test test-parallel spinel-fresh test-pg vendor-examples
|
|
38
|
+
|
|
39
|
+
# Vendor each gem example's Gemfile-declared dependencies via
|
|
40
|
+
# bundler-spinel (spinel-compat vendor, from $(SPINELGEMS)) into
|
|
41
|
+
# examples/<x>/vendor/spinel -- where bin/tep follows the generated
|
|
42
|
+
# deps.rb. This is how a tep app uses a real published gem: declare it in
|
|
43
|
+
# a Gemfile, let spinelgems resolve + place it, require_relative the
|
|
44
|
+
# deps. Run inside the dev container (mounts /spinelgems, sets SPINELGEMS)
|
|
45
|
+
# or point SPINELGEMS at a spinelgems checkout. Regenerated from the
|
|
46
|
+
# committed Gemfile.lock; the output is gitignored.
|
|
47
|
+
SPINELGEMS ?= ../spinelgems
|
|
48
|
+
vendor-examples:
|
|
49
|
+
@for d in examples/*/; do \
|
|
50
|
+
[ -f "$$d/Gemfile.lock" ] || continue; \
|
|
51
|
+
echo "vendor: $$d"; \
|
|
52
|
+
( cd "$$d" && ruby -I "$(SPINELGEMS)/lib" "$(SPINELGEMS)/exe/spinel-compat" vendor ) || exit 1; \
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
# Always check that the local spinel checkout is on tip-of-master
|
|
56
|
+
# before we build / test against it -- spinel moves quickly and a
|
|
57
|
+
# stale binary is a fast way to chase ghost regressions. Skip with
|
|
58
|
+
# `TEP_SKIP_SPINEL_FRESH=1`; override the spinel location with
|
|
59
|
+
# `TEP_SPINEL_DIR`.
|
|
60
|
+
spinel-fresh:
|
|
61
|
+
@$(TEP_ROOT)/tools/spinel-fresh.sh
|
|
62
|
+
|
|
63
|
+
all: spinel-fresh helper hello sinatra_style bench
|
|
64
|
+
|
|
65
|
+
helper: spinel-fresh $(LIB_DIR)/sphttp.o $(LIB_DIR)/tep_sqlite.o $(LIB_DIR)/tep_pg.o
|
|
66
|
+
|
|
67
|
+
$(LIB_DIR)/sphttp.o: $(LIB_DIR)/sphttp.c
|
|
68
|
+
cc -O2 -c $< -o $@
|
|
69
|
+
|
|
70
|
+
$(LIB_DIR)/tep_sqlite.o: $(LIB_DIR)/tep_sqlite.c
|
|
71
|
+
cc -O2 -c $< -o $@
|
|
72
|
+
|
|
73
|
+
$(LIB_DIR)/tep_pg.o: $(LIB_DIR)/tep_pg.c
|
|
74
|
+
cc -O2 -c $(TEP_PG_CFLAGS) $< -o $@
|
|
75
|
+
|
|
76
|
+
hello: helper
|
|
77
|
+
$(TEP) build examples/hello.rb
|
|
78
|
+
|
|
79
|
+
sinatra_style: helper
|
|
80
|
+
$(TEP) build examples/sinatra_style.rb
|
|
81
|
+
|
|
82
|
+
bench: bench-tep
|
|
83
|
+
|
|
84
|
+
bench-tep: helper
|
|
85
|
+
$(TEP) build bench/hello_bench.rb
|
|
86
|
+
$(TEP) build bench/api_bench.rb
|
|
87
|
+
$(TEP) build bench/pg_bench.rb
|
|
88
|
+
|
|
89
|
+
bench-sinatra:
|
|
90
|
+
cd bench && bundle _2.7.2_ install --quiet
|
|
91
|
+
|
|
92
|
+
demo: hello
|
|
93
|
+
./examples/hello
|
|
94
|
+
|
|
95
|
+
test: helper
|
|
96
|
+
@pkill -f tep-test 2>/dev/null; true
|
|
97
|
+
ruby test/run_all.rb
|
|
98
|
+
|
|
99
|
+
# `make test-parallel` -- dev fast loop. Runs each test/test_*.rb in
|
|
100
|
+
# its own process in parallel (sidestepping the harness's "one thread
|
|
101
|
+
# per class" constraint), with per-process port-base allocation. Cuts
|
|
102
|
+
# wall time roughly by core count on the gx10 (~13min serial ->
|
|
103
|
+
# ~1-2min). `make test` stays serial -- the canonical / CI path with
|
|
104
|
+
# clean output ordering. Cap with TEP_TEST_PROCS=N.
|
|
105
|
+
test-parallel: helper
|
|
106
|
+
@pkill -f tep-test 2>/dev/null; true
|
|
107
|
+
ruby test/run_parallel.rb
|
|
108
|
+
|
|
109
|
+
# `make test-pg` -- runs test/test_pg.rb against a real PostgreSQL.
|
|
110
|
+
# Reads PG_TEST_URL from the environment if set; otherwise expects
|
|
111
|
+
# the caller to have a PG reachable at the default libpq path.
|
|
112
|
+
# Without PG_TEST_URL the test class skips cleanly (so `make test`
|
|
113
|
+
# is unaffected by Postgres availability).
|
|
114
|
+
#
|
|
115
|
+
# Recipe to spin up a throwaway PG via docker:
|
|
116
|
+
#
|
|
117
|
+
# docker run -d --rm --name tep_test_pg -p 54329:5432 \
|
|
118
|
+
# -e POSTGRES_PASSWORD=postgres postgres:16
|
|
119
|
+
# until docker exec tep_test_pg pg_isready -U postgres | grep -q accepting; do sleep 1; done
|
|
120
|
+
# PG_TEST_URL='postgresql://postgres:postgres@127.0.0.1:54329/postgres' make test-pg
|
|
121
|
+
# docker stop tep_test_pg
|
|
122
|
+
test-pg: helper
|
|
123
|
+
@pkill -f tep-test 2>/dev/null; true
|
|
124
|
+
ruby test/test_pg.rb
|
|
125
|
+
|
|
126
|
+
clean:
|
|
127
|
+
rm -f $(LIB_DIR)/*.o
|
|
128
|
+
rm -f examples/hello examples/sinatra_style examples/diag
|
|
129
|
+
rm -f examples/.*.tep.rb
|
|
130
|
+
rm -f bench/hello_bench bench/api_bench bench/.*.tep.rb
|
|
131
|
+
rm -f test/real_world/.*.tep.rb
|
|
132
|
+
# Compiled binaries in test/real_world/ have no extension; sources
|
|
133
|
+
# are .rb. Find executables and remove only those.
|
|
134
|
+
@find test/real_world -maxdepth 1 -type f -perm -u+x ! -name '*.rb' -delete 2>/dev/null || true
|
data/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo/tep.png" alt="Tep — Spinal Tap of web frameworks" width="240">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Tep
|
|
6
|
+
|
|
7
|
+
A Sinatra-flavoured web framework that compiles to a native binary
|
|
8
|
+
via [Spinel][spinel].
|
|
9
|
+
|
|
10
|
+
> **Current release:** [v0.10.0](https://github.com/OriPekelman/tep/releases/tag/v0.10.0)
|
|
11
|
+
> — the Proxy + OpenAI-server + Events batteries on top of the agentic
|
|
12
|
+
> surface (Auth, Broadcast, Presence, LiveView, MCP).
|
|
13
|
+
> Pre-alpha; API still in motion.
|
|
14
|
+
|
|
15
|
+
> **Why Tep exists.** Two complementary goals:
|
|
16
|
+
>
|
|
17
|
+
> 1. **Exercise Spinel against real-world Ruby code.** Tep is the
|
|
18
|
+
> largest pure-Ruby application Spinel compiles end-to-end. Every
|
|
19
|
+
> Sinatra idiom, every battery, every demo doubles as a torture
|
|
20
|
+
> test for the AOT compiler's codegen + analyzer. Bugs surface
|
|
21
|
+
> here, get reduced to minimal repros, and land upstream as PRs
|
|
22
|
+
> or issues against [matz/spinel](https://github.com/matz/spinel).
|
|
23
|
+
> If something doesn't work in Tep, the bug is usually in Spinel,
|
|
24
|
+
> and that's the point.
|
|
25
|
+
>
|
|
26
|
+
> 2. **Be the harness for [toy][toy].** Toy is Tep's sibling
|
|
27
|
+
> project: a machine-learning framework written in pure Ruby and
|
|
28
|
+
> compiled by Spinel. Toy needs an HTTP/MCP layer for serving
|
|
29
|
+
> models, exposing training tools to agents (Claude Code et al.),
|
|
30
|
+
> streaming inference results, and wiring presence into
|
|
31
|
+
> collaborative training sessions. Tep is that layer. Every
|
|
32
|
+
> battery in Tep is shaped by what Toy actually needs to ship.
|
|
33
|
+
>
|
|
34
|
+
> Tep happens to be useful as a general web framework too — fast,
|
|
35
|
+
> single-binary, Sinatra-shaped — but the design choices are made
|
|
36
|
+
> through these two lenses.
|
|
37
|
+
|
|
38
|
+
[toy]: https://github.com/OriPekelman/toy
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
# 1. install Spinel
|
|
44
|
+
git clone https://github.com/matz/spinel
|
|
45
|
+
cd spinel && make all
|
|
46
|
+
export PATH="$PWD:$PATH"
|
|
47
|
+
|
|
48
|
+
# 2. install Tep
|
|
49
|
+
git clone https://github.com/OriPekelman/tep
|
|
50
|
+
cd tep && make all
|
|
51
|
+
./examples/hello -p 4567
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Your own app:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# hello.rb
|
|
58
|
+
require 'sinatra'
|
|
59
|
+
|
|
60
|
+
get '/' do
|
|
61
|
+
"hello from Tep"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
get '/hi/:name' do
|
|
65
|
+
"hi, " + params[:name] + "!"
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
tep build hello.rb # -> ./hello (~80 KB binary, no Ruby runtime)
|
|
71
|
+
./hello -p 4567
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The translator (`bin/tep`) needs CRuby >= 3.2 with the `prism` gem
|
|
75
|
+
installed (a development-only dependency — `bundle install` brings it
|
|
76
|
+
in). Prism ships with the stdlib from 3.4 onward; on 3.2/3.3 it's a
|
|
77
|
+
gem install. Recommended Ruby manager:
|
|
78
|
+
[`rv`](https://github.com/spinel-coop/rv) — fast version+gem
|
|
79
|
+
manager from the Spinel Cooperative (separate project from the
|
|
80
|
+
matz/spinel AOT compiler Tep compiles through; same Ruby
|
|
81
|
+
neighbourhood). `.ruby-version` in this repo pins 3.4.0; `rv
|
|
82
|
+
shell` makes `rv run rake test` just work. Build deps on Linux:
|
|
83
|
+
`build-essential`, `libsqlite3-dev`. macOS: Xcode CLI tools.
|
|
84
|
+
|
|
85
|
+
For a full walkthrough — auth, persistence, deploy — see the
|
|
86
|
+
[Getting started](https://github.com/OriPekelman/tep/wiki/Getting-Started)
|
|
87
|
+
wiki page.
|
|
88
|
+
|
|
89
|
+
## Is it fast?
|
|
90
|
+
|
|
91
|
+
Yes. ~150k req/s on a small Linux server with the request path doing
|
|
92
|
+
actual work (SQLite SELECT + JSON), microsecond median latency. The
|
|
93
|
+
hot path is C from end to end (epoll, request parsing, dispatch,
|
|
94
|
+
response writer); the Ruby you wrote is compiled, not interpreted.
|
|
95
|
+
HTTP/1.1 keep-alive and prefork with `SO_REUSEPORT` are the usual
|
|
96
|
+
wins applied.
|
|
97
|
+
|
|
98
|
+
Two scenarios, both `wrk -t8 -c256 -d10s` on Linux 6.x / aarch64,
|
|
99
|
+
8 workers a side (Sinatra: 8 workers × 4 threads):
|
|
100
|
+
|
|
101
|
+
| Scenario | Server | Req/sec | p50 | p99 |
|
|
102
|
+
|-------------------------|----------------|--------:|-------:|-------:|
|
|
103
|
+
| **hello** (raw plumbing)| Tep | 167,150 | 40 µs | <1 ms |
|
|
104
|
+
| hello | Sinatra + Puma | 31,184 | 40 ms | 171 ms |
|
|
105
|
+
| **api** (SQLite + JSON) | Tep | 145,290 | 43 µs | 243 µs |
|
|
106
|
+
| api | Sinatra + Puma | 24,926 | 1.8 ms | 171 ms |
|
|
107
|
+
|
|
108
|
+
Numbers are conservative floors; a clean re-run on a quiet host is
|
|
109
|
+
expected to come in higher. Reproduce with `bench/run_all.sh`.
|
|
110
|
+
|
|
111
|
+
> **macOS note.** Linux is Tep's primary deployment target. Builds
|
|
112
|
+
> and runs on macOS too, but Darwin's `SO_REUSEPORT` doesn't load-
|
|
113
|
+
> balance new connections across prefork workers — a single long-
|
|
114
|
+
> running response on the busy worker blocks every other request on
|
|
115
|
+
> the same listener. On Linux 3.9+ the kernel distributes accepts
|
|
116
|
+
> correctly, so prefork scales as the table above. The path forward
|
|
117
|
+
> for tep apps that need real concurrency on macOS is
|
|
118
|
+
> `Tep::Server::Scheduled` (one worker, fibers per connection) + a
|
|
119
|
+
> cooperative `Tep::Http` — design + phases in
|
|
120
|
+
> [`docs/MACOS-CONCURRENCY.md`](docs/MACOS-CONCURRENCY.md).
|
|
121
|
+
|
|
122
|
+
## What's in the box
|
|
123
|
+
|
|
124
|
+
Sinatra-shaped routing, filters, responses, templates, sessions,
|
|
125
|
+
plus a collection of "batteries" — pure-Tep modules that cover the
|
|
126
|
+
gem ecosystem's most common needs in a way that lowers cleanly
|
|
127
|
+
through Spinel.
|
|
128
|
+
|
|
129
|
+
| Battery | What it covers |
|
|
130
|
+
|------------------|---|
|
|
131
|
+
| `Tep::SQLite` | libsqlite3 wrapper via a small C shim — exec / prepare / bind / step / col / first_str / first_int. |
|
|
132
|
+
| `Tep::Json` | encode primitives + flat-key decoder for JSON-over-HTTP. |
|
|
133
|
+
| `Tep::Logger` | levelled logger (debug/info/warn/error), stderr or file. |
|
|
134
|
+
| `Tep::Jwt` | HS256 JWT encode / verify / decode. |
|
|
135
|
+
| `Tep::Password` | PBKDF2-SHA256, 200k iters, self-describing storage. |
|
|
136
|
+
| `Tep::Security` | `Cors` (before-filter) + `Headers` (HSTS, nosniff, ...). |
|
|
137
|
+
| `Tep::Assets` | compile-time bundling for `<app>/assets/*`. |
|
|
138
|
+
| `Tep::Scheduler` | cooperative fiber scheduler with timer + I/O parking. |
|
|
139
|
+
| `Tep::Shell` | popen-based shell-out + small-file reader (`/proc`, `/sys`, `/etc`). |
|
|
140
|
+
| `Tep::Http` | Faraday-shaped outbound HTTP/1.0 client. |
|
|
141
|
+
| `Tep::Llm` | ruby-openai-shaped chat-completions client; backends interchangeable via base_url (Ollama / OpenAI / [toy](https://github.com/OriPekelman/toy)). Sync `chat()` + SSE `chat_stream()`. |
|
|
142
|
+
| `Tep::Llm::OpenAI::Server` | The other side of the OpenAI fence — serve OpenAI-compatible HTTP from local compute. Subclass `Tep::Llm::OpenAI::Backend`, wire `Server.use(MyBackend.new)` + `Server.serve!(events_jsonl)`, get `/v1/models` + `/v1/completions` (non-streaming + SSE with `"stream": true`) + toy/v1 events out of the box. `/v1/embeddings` lives app-side (the lib stays float-free); see [`docs/OPENAI-SERVER-BATTERY.md`](docs/OPENAI-SERVER-BATTERY.md). |
|
|
143
|
+
| `Tep::Proxy` | Sinatra-flavored reverse proxy. Subclass `Tep::Proxy`, override `rewrite_path` / `before_forward` / `after_forward` (non-streaming), or `stream_request?` / `on_stream_chunk` / `on_stream_end` (SSE / chunked pass-through). Block-form DSL — `proxy.before_forward do \|req, res, ureq\| ... end` — lowers to a synthesized subclass; see [`docs/PROXY-BATTERY.md`](docs/PROXY-BATTERY.md). |
|
|
144
|
+
| `Tep::Events` | toy/v1 JSONL emitter (`run_start` / `inference` / `run_end`). Float-free by design — caller hands in integer `wall_us`, schema-side floats live in caller-built `extra_json`. The OpenAI-server battery wires it automatically; standalone for any per-request telemetry stream. |
|
|
145
|
+
| `Tep::WebSocket` | RFC 6455 server-side WebSocket. `websocket '/chat' do \|ws\| ... end` DSL lowers to Frame + Handshake + Driver + Connection. Requires `set :scheduler, :scheduled`. |
|
|
146
|
+
| `Tep::Parallel` | grosser/parallel-shaped fork fan-out. |
|
|
147
|
+
| `Tep::Job` | sidekiq-shaped queue over SQLite. |
|
|
148
|
+
| `PG` | ruby-pg-shape libpq client: `PG::Connection`, `PG::Result`, `PG::Error`; surface mirrors the `pg` gem (`exec` / `exec_params` / `escape_*` / `fields` / `values` / `getvalue` / `sql_state`). Designed so an eventual ActiveRecord-on-spinel port reuses the existing AR adapter with minimal divergence — see `docs/PG-BATTERY.md`. |
|
|
149
|
+
| `Tep::Auth` | Principal+delegate identity (`Tep::Identity` / `Tep::AgentDelegation`) + provider chain. Three providers shipped: `Tep::AuthBearerToken` (JWT-HS256), `Tep::AuthSessionCookie` (signed cookie), `Tep::AuthOAuth2` (authorization-code grant issuance for bots/agents). Same `req.identity` surface regardless of provider; agents are first-class (`identity.agent?`, `identity.acting_via.agent_id`, capability subsetting). |
|
|
150
|
+
| `Tep::Broadcast` | In-process pub-sub + cross-worker via PG LISTEN/NOTIFY. Subscribe an fd to a topic (`subscribe` raw, `subscribe_ws` WS-frame-wrapped); publish writes to every matching subscriber. The seam Presence and LiveView build on. |
|
|
151
|
+
| `Tep::Presence` | Topic-keyed who's-here registry, agent-aware. `Tep::Presence.track(req, topic, fd)` records a (principal, session, topic) tuple with a 3-state structured status (`:available | :busy | :blocked` + free-text note + expiry). Diffs broadcast on join/leave/status; PG-mirror for cross-worker `list_global` snapshots. |
|
|
152
|
+
| `Tep::LiveView` | Phoenix.LiveView-shape server-rendered stateful UI over WebSocket. Subclass `Tep::LiveView`, override `render` + `handle_event` + (optionally) `handle_presence_diff`; `broadcast_render` fans the new HTML out to every subscribed viewer. Bootstrap client (~10 lines of inline JS) ships in `Tep::LiveView.render_page`. |
|
|
153
|
+
| `Tep::MCP` | Tool catalog for the agent-as-driver role. `mcp_tool 'name', "desc" do; param :foo, Type, "..."; on_call do; ...; end; end` registers a tool both at `POST /tools/<name>` (HTTP-direct) and through a JSON-RPC 2.0 dispatcher at `POST /mcp` (MCP-native — Claude Code / OpenCode / Gravity CLI). `GET /llms.txt` auto-publishes the catalog. See [`docs/MCP-BATTERY.md`](docs/MCP-BATTERY.md). Chunk 5.1; `mcp_resource` + streaming + OpenAPI in 5.2–5.4. |
|
|
154
|
+
|
|
155
|
+
Per-battery API docs and cookbooks live on the
|
|
156
|
+
[wiki](https://github.com/OriPekelman/tep/wiki). The full
|
|
157
|
+
Sinatra-compatibility matrix is in
|
|
158
|
+
[SINATRA_COMPAT.md](SINATRA_COMPAT.md).
|
|
159
|
+
|
|
160
|
+
The last four batteries (`Tep::Auth`, `Tep::Broadcast`,
|
|
161
|
+
`Tep::Presence`, `Tep::LiveView`) ship a small framework for
|
|
162
|
+
"web apps in a live agentic age" — `req.identity` is always a
|
|
163
|
+
principal+delegate pair so agents acting on behalf of humans
|
|
164
|
+
are first-class through every battery. The end-to-end design
|
|
165
|
+
+ a realistic chat-room scenario walked through every seam
|
|
166
|
+
lives in [`docs/BATTERIES-DESIGN.md`](docs/BATTERIES-DESIGN.md).
|
|
167
|
+
|
|
168
|
+
430 tests pass `make test` (serial, ~13 min on the gx10) or `make test-parallel` (subprocess fan-out, ~4 min — see [`test/run_parallel.rb`](test/run_parallel.rb)). End-to-end demos that build and run:
|
|
169
|
+
|
|
170
|
+
- **[`examples/counter/`](examples/counter/app.rb)** — the
|
|
171
|
+
smallest `Tep.live` demo. Shared integer counter; click in one
|
|
172
|
+
tab, every other tab updates in <100ms. ~80 lines of Ruby + CSS,
|
|
173
|
+
no JS to write (the bootstrap shell wires `data-event` clicks
|
|
174
|
+
through the WS).
|
|
175
|
+
- **[`examples/experiments/`](examples/experiments/app.rb)** —
|
|
176
|
+
the `Tep::MCP` battery demo. Mock training-run manager driven
|
|
177
|
+
by an MCP client (Claude Code / OpenCode / Gravity): 4 tools,
|
|
178
|
+
2 resources, capability gating, auto-published `/llms.txt` +
|
|
179
|
+
`/openapi.json` + `/mcp` JSON-RPC. ~200 lines of Ruby plus an
|
|
180
|
+
`AGENTS.md` worked example.
|
|
181
|
+
- **[`examples/agentic_chat/`](examples/agentic_chat/app.rb)** —
|
|
182
|
+
the four-battery agentic demo. Sub-second WS push, multi-user
|
|
183
|
+
chat, agent-spawn with OAuth2-style delegation. ~270 lines.
|
|
184
|
+
- **[`examples/chatbot/`](examples/chatbot/app.rb)** — minimalistic
|
|
185
|
+
OpenWebUI-style client backed by any OpenAI-compatible endpoint
|
|
186
|
+
(Ollama / OpenAI / [toy](https://github.com/OriPekelman/toy)'s `toy serve`)
|
|
187
|
+
exercising the full pre-agentic battery surface
|
|
188
|
+
(`Tep::Server::Scheduled` + `Tep::Llm` + `Tep::SQLite` +
|
|
189
|
+
`Tep::Streamer` + `Tep::Session` + `Tep::Password` + `Tep::Jwt` +
|
|
190
|
+
`Tep::Security::{Cors,Headers}` + `Tep::Assets` + `Tep::Json` +
|
|
191
|
+
`Tep::Job` + `Tep::Logger`) in ~1500 lines of Ruby + HTML + CSS + JS.
|
|
192
|
+
- **[`examples/llm_gateway/`](examples/llm_gateway/app.rb)** — the
|
|
193
|
+
`Tep::Proxy` streaming demo. Block-form DSL gateway in front of an
|
|
194
|
+
OpenAI-compatible upstream; pumps SSE token deltas straight through
|
|
195
|
+
to the client and emits one `Tep::Events` `inference` event per
|
|
196
|
+
completed stream.
|
|
197
|
+
- **[`examples/api_gateway/`](examples/api_gateway/app.rb)** — the
|
|
198
|
+
buffered-proxy demo. Capability-gated forwarding with `before_forward`
|
|
199
|
+
+ `after_forward` observability hooks; same DSL, non-streaming.
|
|
200
|
+
- **[`examples/websocket_echo.rb`](examples/websocket_echo.rb)** —
|
|
201
|
+
`Tep::WebSocket` in isolation; `test/test_websocket_echo.rb`
|
|
202
|
+
performs a real RFC 6455 handshake over a raw socket and
|
|
203
|
+
round-trips a masked TEXT frame.
|
|
204
|
+
|
|
205
|
+
### Type signatures (RBS)
|
|
206
|
+
|
|
207
|
+
`sig/` ships [RBS](https://github.com/ruby/rbs) signatures for tep's
|
|
208
|
+
public surface, mirroring `lib/tep/`. They're for IDE tooling today
|
|
209
|
+
(Solargraph, RubyMine) and for forward compatibility with
|
|
210
|
+
spinel-side RBS consumption (discussion at [#6](https://github.com/OriPekelman/tep/issues/6)) —
|
|
211
|
+
the goal is to let library authors carry the type-correctness burden
|
|
212
|
+
in `.rbs` files so app developers can write idiomatic Ruby without
|
|
213
|
+
the inference-warming seed dance that currently lives at the top
|
|
214
|
+
of `lib/tep.rb`. `rake rbs:validate` syntax-checks the tree.
|
|
215
|
+
|
|
216
|
+
## Spinel-direct
|
|
217
|
+
|
|
218
|
+
`tep build` accepts either the Sinatra DSL or a lower-level
|
|
219
|
+
Tep-class style without the translator:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
require_relative '../lib/tep'
|
|
223
|
+
|
|
224
|
+
class Hi < Tep::Handler
|
|
225
|
+
def handle(req, res)
|
|
226
|
+
"<p>hi, " + req.params["name"] + "!</p>"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
Tep.get "/hi/:name", Hi.new
|
|
231
|
+
Tep.run!(4567, 1, false)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Useful for tracing where the translator's textual rewrites go.
|
|
235
|
+
|
|
236
|
+
## Reporting bugs
|
|
237
|
+
|
|
238
|
+
Tep deliberately exists to find Spinel's edges. If you hit a
|
|
239
|
+
Sinatra idiom that doesn't translate, a Spinel-emitted miscompile,
|
|
240
|
+
a runtime hang — please file an issue with a minimal reproduction.
|
|
241
|
+
"Your app doesn't build" is a useful data point.
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT, see [LICENSE](LICENSE).
|
|
246
|
+
|
|
247
|
+
[spinel]: https://github.com/matz/spinel
|