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.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. 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