tep 0.11.5 → 0.11.6
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 +4 -4
- data/README.md +34 -30
- data/bin/tep +10 -2
- data/examples/pg_hello.rb +9 -8
- data/lib/tep/pg.rb +13 -28
- data/lib/tep/proxy.rb +2 -2
- data/lib/tep/shell.rb +17 -6
- data/lib/tep/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33b663dcc38571d71f8a729f7ed1f8da6ea8a89e1eb6bd49619beac977da2727
|
|
4
|
+
data.tar.gz: af7b47e7e3440372325744d1274e70e41216db757ac224ca2364c1a422b5064a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed5ce816c591c5464d624e3094798d3ce8c8c03c7128e3881541cf874a0735e62f468d7d4f781e793e6ae02f2bcb642119fdc49157394acb69cb027a014d9b6e
|
|
7
|
+
data.tar.gz: 484a1cdae65a65cbe5da98d8c2aa22add1b4a562e35ab7cfb38f9137856bcad77ad773b07d191fb425a5e6307fe18a71856d47ad3766e8a03191622314ba2989
|
data/README.md
CHANGED
|
@@ -4,37 +4,41 @@
|
|
|
4
4
|
|
|
5
5
|
# Tep
|
|
6
6
|
|
|
7
|
-
A Sinatra-flavoured web framework that compiles to a native binary
|
|
8
|
-
via [Spinel][spinel].
|
|
7
|
+
A Sinatra-flavoured web framework that compiles to a **native binary**
|
|
8
|
+
via [Spinel][spinel]. You write a Sinatra-style `app.rb`:
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
```ruby
|
|
11
|
+
require 'sinatra'
|
|
12
|
+
|
|
13
|
+
get '/hi/:name' do
|
|
14
|
+
"hi, " + params[:name] + "!"
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
…and get `./app` — a single static executable, ~80 KB, **no Ruby
|
|
19
|
+
runtime**, doing ~150k req/s. Beyond routing / sessions / templates, the
|
|
20
|
+
[batteries ↓](#whats-in-the-box) cover auth (bearer / cookie / OAuth2),
|
|
21
|
+
SQLite, opt-in PostgreSQL, WebSockets, Broadcast + Presence + LiveView,
|
|
22
|
+
an MCP tool catalog, TLS, a pooled HTTP client, and an OpenAI-compatible
|
|
23
|
+
LLM client + server.
|
|
24
|
+
|
|
25
|
+
> **Current release:** [v0.11.6](https://github.com/OriPekelman/tep/releases/tag/v0.11.6)
|
|
26
|
+
> on [RubyGems](https://rubygems.org/gems/tep) — `gem install tep`.
|
|
14
27
|
> Pre-alpha; API still in motion.
|
|
15
28
|
|
|
16
|
-
>
|
|
17
|
-
>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
>
|
|
29
|
-
> compiled by Spinel. Toy needs an HTTP/MCP layer for serving
|
|
30
|
-
> models, exposing training tools to agents (Claude Code et al.),
|
|
31
|
-
> streaming inference results, and wiring presence into
|
|
32
|
-
> collaborative training sessions. Tep is that layer. Every
|
|
33
|
-
> battery in Tep is shaped by what Toy actually needs to ship.
|
|
34
|
-
>
|
|
35
|
-
> Tep happens to be useful as a general web framework too — fast,
|
|
36
|
-
> single-binary, Sinatra-shaped — but the design choices are made
|
|
37
|
-
> through these two lenses.
|
|
29
|
+
<details>
|
|
30
|
+
<summary><b>Why Tep exists</b> — it's a deliberate Spinel torture test + toy's serving layer</summary>
|
|
31
|
+
|
|
32
|
+
Tep is the largest pure-Ruby application Spinel compiles end-to-end:
|
|
33
|
+
every Sinatra idiom and battery doubles as a torture test for the AOT
|
|
34
|
+
compiler's codegen + analyzer, and bugs found here get reduced to
|
|
35
|
+
minimal repros and land upstream as
|
|
36
|
+
[matz/spinel](https://github.com/matz/spinel) PRs. It's also the
|
|
37
|
+
HTTP / MCP / serving layer for its sibling [toy][toy] — a pure-Ruby ML
|
|
38
|
+
framework Spinel compiles — so every battery is shaped by what toy
|
|
39
|
+
needs to ship. Tep is a genuinely useful general web framework too, but
|
|
40
|
+
the design choices are made through those two lenses.
|
|
41
|
+
</details>
|
|
38
42
|
|
|
39
43
|
[toy]: https://github.com/OriPekelman/toy
|
|
40
44
|
|
|
@@ -107,7 +111,7 @@ gem install. Recommended Ruby manager:
|
|
|
107
111
|
[`rv`](https://github.com/spinel-coop/rv) — fast version+gem
|
|
108
112
|
manager from the Spinel Cooperative (separate project from the
|
|
109
113
|
matz/spinel AOT compiler Tep compiles through; same Ruby
|
|
110
|
-
neighbourhood). `.ruby-version` in this repo pins
|
|
114
|
+
neighbourhood). `.ruby-version` in this repo pins 4.0.0; `rv
|
|
111
115
|
shell` makes `rv run rake test` just work. Build deps on Linux:
|
|
112
116
|
`build-essential`, `libsqlite3-dev`. macOS: Xcode CLI tools.
|
|
113
117
|
|
|
@@ -174,7 +178,7 @@ through Spinel.
|
|
|
174
178
|
| `Tep::WebSocket` | RFC 6455 server-side WebSocket. `websocket '/chat' do \|ws\| ... end` DSL lowers to Frame + Handshake + Driver + Connection. Requires `set :scheduler, :scheduled`. |
|
|
175
179
|
| `Tep::Parallel` | grosser/parallel-shaped fork fan-out. |
|
|
176
180
|
| `Tep::Job` | sidekiq-shaped queue over SQLite. |
|
|
177
|
-
| `PG` | ruby-pg-shape libpq client
|
|
181
|
+
| `PG` | **Opt-in** ruby-pg-shape libpq client — `require "tep/pg"` (so non-PG apps don't link libpq). `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`. |
|
|
178
182
|
| `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). |
|
|
179
183
|
| `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. |
|
|
180
184
|
| `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. |
|
data/bin/tep
CHANGED
|
@@ -1610,7 +1610,14 @@ def rewrite_block(src, force_string: true)
|
|
|
1610
1610
|
# are handled separately so they can contain commas or spaces.
|
|
1611
1611
|
pairs = $2.scan(/(\w+):\s*("[^"]*"|'[^']*'|[^,}]+?)\s*(?=,|\z)/)
|
|
1612
1612
|
setters = pairs.map { |k, v| %{__l["#{k}"] = (#{v.strip}).to_s} }.join("; ")
|
|
1613
|
-
|
|
1613
|
+
# Build the locals hash INSIDE the first argument (a sequence
|
|
1614
|
+
# expression that returns __l), rather than `(__l = ...; render(__l))`
|
|
1615
|
+
# with __l a bare arg. In the latter shape spinel (recent master)
|
|
1616
|
+
# hoists the `render`'s arg temp -- `_t = __l` -- above the in-sequence
|
|
1617
|
+
# `__l = str_hash()` assignment, so render receives the pre-assignment
|
|
1618
|
+
# (empty) hash and every interpolated value blanks (matz/spinel#1478).
|
|
1619
|
+
# Keeping the assignment inside the arg evaluation defeats the hoist.
|
|
1620
|
+
"tep_view_#{view}((__l = Tep.str_hash; #{setters}; __l), req.ivars)"
|
|
1614
1621
|
end
|
|
1615
1622
|
s = s.gsub(/(?<![\w.])erb\s+:(\w+)/, 'tep_view_\1(Tep.str_hash, req.ivars)')
|
|
1616
1623
|
|
|
@@ -1619,7 +1626,8 @@ def rewrite_block(src, force_string: true)
|
|
|
1619
1626
|
view = $1
|
|
1620
1627
|
pairs = $2.scan(/(\w+):\s*("[^"]*"|'[^']*'|[^,}]+?)\s*(?=,|\z)/)
|
|
1621
1628
|
setters = pairs.map { |k, v| %{__l["#{k}"] = (#{v.strip}).to_s} }.join("; ")
|
|
1622
|
-
|
|
1629
|
+
# Same arg-hoist avoidance as the erb form above (matz/spinel#1478).
|
|
1630
|
+
"tep_mustache_#{view}((__l = Tep.str_hash; #{setters}; __l), req.ivars)"
|
|
1623
1631
|
end
|
|
1624
1632
|
s = s.gsub(/(?<![\w.])mustache\s+:(\w+)/, 'tep_mustache_\1(Tep.str_hash, req.ivars)')
|
|
1625
1633
|
|
data/examples/pg_hello.rb
CHANGED
|
@@ -68,15 +68,16 @@ get '/error' do
|
|
|
68
68
|
out = "rescued PG::UndefinedTable\n" +
|
|
69
69
|
"sqlstate: " + c.last_sqlstate + "\n" +
|
|
70
70
|
"is undefined-table? " + (c.last_sqlstate == "42P01" ? "yes" : "no") + "\n" +
|
|
71
|
-
# WORKAROUND --
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
71
|
+
# WORKAROUND -- still open at SPINEL_PIN (re-checked at the
|
|
72
|
+
# ad2b71ad re-pin: `e.is_a?(PG::Error)` here is rejected as
|
|
73
|
+
# `unsupported call: is_a? recv=LocalVariableRead argc=1`).
|
|
74
|
+
# `e` is the rescued exception, typed PG::UndefinedTable -- a
|
|
75
|
+
# whole-program is_a? against the namespaced ancestor PG::Error
|
|
76
|
+
# isn't lowered yet. Minimal `rescue Sub => e; e.is_a?(Super)`
|
|
77
|
+
# compiles fine; only the full program trips it. Since `e` is
|
|
78
|
+
# always a PG::Error subclass here, hardcode "yes". Restore
|
|
79
79
|
# (e.is_a?(PG::Error) ? "yes" : "no")
|
|
80
|
+
# once is_a?-on-rescued-namespaced-ancestor lowers. See tep#196.
|
|
80
81
|
"is PG::Error? " + "yes" + "\n" +
|
|
81
82
|
"message: " + e.message
|
|
82
83
|
end
|
data/lib/tep/pg.rb
CHANGED
|
@@ -197,34 +197,19 @@ module PG
|
|
|
197
197
|
h = Pg.tep_pg_connect(opts)
|
|
198
198
|
end
|
|
199
199
|
else
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# upstream narrowing fix lands, and re-add Hash-form test
|
|
214
|
-
# coverage. Until then a Hash arg yields a failed Connection
|
|
215
|
-
# (connected? == false) rather than a miscompile.
|
|
216
|
-
#
|
|
217
|
-
# keys = ""
|
|
218
|
-
# vals = ""
|
|
219
|
-
# n = 0
|
|
220
|
-
# opts.each do |k, v|
|
|
221
|
-
# keys = keys + k + "\0"
|
|
222
|
-
# vals = vals + v + "\0"
|
|
223
|
-
# n += 1
|
|
224
|
-
# end
|
|
225
|
-
# h = Pg.tep_pg_connect_kv(keys, vals, n)
|
|
226
|
-
h = -1
|
|
227
|
-
# =================================================================
|
|
200
|
+
# Hash-conninfo form: pack the key/value pairs into NUL-delimited
|
|
201
|
+
# buffers for the C shim. (`opts` narrows to Hash in this
|
|
202
|
+
# is_a?(String) ELSE branch -- the narrowing gap that blocked the
|
|
203
|
+
# re-pin, matz/spinel#1434, is fixed as of the SPINEL_PIN bump.)
|
|
204
|
+
keys = ""
|
|
205
|
+
vals = ""
|
|
206
|
+
n = 0
|
|
207
|
+
opts.each do |k, v|
|
|
208
|
+
keys = keys + k + "\0"
|
|
209
|
+
vals = vals + v + "\0"
|
|
210
|
+
n += 1
|
|
211
|
+
end
|
|
212
|
+
h = Pg.tep_pg_connect_kv(keys, vals, n)
|
|
228
213
|
end
|
|
229
214
|
if h < 0
|
|
230
215
|
# Slot 0 holds the most recent connect-failure error message
|
data/lib/tep/proxy.rb
CHANGED
|
@@ -62,7 +62,7 @@ module Tep
|
|
|
62
62
|
# wins (whichever setter you called second).
|
|
63
63
|
#
|
|
64
64
|
# Default shape: max_attempts=1 (no retry, back-compat).
|
|
65
|
-
class Proxy
|
|
65
|
+
class Proxy < Tep::Handler
|
|
66
66
|
class RetryPolicy
|
|
67
67
|
attr_accessor :max_attempts, :base_backoff_ms, :backoff_multiplier
|
|
68
68
|
attr_accessor :retry_on_status
|
|
@@ -125,7 +125,7 @@ module Tep
|
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
-
class Proxy
|
|
128
|
+
class Proxy
|
|
129
129
|
attr_accessor :upstream, :timeout
|
|
130
130
|
# Body size caps (chunk 6.6). max_request_body_bytes bounds the
|
|
131
131
|
# inbound body the proxy will accept (over -> 413 Payload Too
|
data/lib/tep/shell.rb
CHANGED
|
@@ -36,18 +36,29 @@ module Tep
|
|
|
36
36
|
|
|
37
37
|
# Read a file's contents. Useful for /proc/loadavg, /proc/meminfo,
|
|
38
38
|
# /sys/class/thermal/.../temp, and similar small-text endpoints.
|
|
39
|
-
# Returns "" on open failure
|
|
40
|
-
#
|
|
41
|
-
#
|
|
39
|
+
# Returns "" on open failure. Spinel's File.read now raises a
|
|
40
|
+
# CRuby-correct Errno on a missing/unreadable path (it used to
|
|
41
|
+
# silently return ""); we rescue here to preserve this helper's
|
|
42
|
+
# never-raise contract -- a handler reading /proc should get "" for
|
|
43
|
+
# an absent file, not a 502'd worker.
|
|
42
44
|
def self.read(path)
|
|
43
|
-
|
|
45
|
+
begin
|
|
46
|
+
File.read(path)
|
|
47
|
+
rescue => e
|
|
48
|
+
""
|
|
49
|
+
end
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
# Bounded read: slice after the fact. The cap is mostly a
|
|
47
53
|
# defensive cue -- callers that need it should be reading
|
|
48
|
-
# bounded /proc files anyway.
|
|
54
|
+
# bounded /proc files anyway. Same never-raise contract as #read.
|
|
49
55
|
def self.read_limited(path, max_bytes)
|
|
50
|
-
out =
|
|
56
|
+
out = ""
|
|
57
|
+
begin
|
|
58
|
+
out = File.read(path)
|
|
59
|
+
rescue => e
|
|
60
|
+
out = ""
|
|
61
|
+
end
|
|
51
62
|
out.length > max_bytes ? out[0, max_bytes] : out
|
|
52
63
|
end
|
|
53
64
|
|
data/lib/tep/version.rb
CHANGED