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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0f6e0ace2299c85ad62734b3af3b5b5a704994d2c0769f3151f1741b2c7ec26
4
- data.tar.gz: 0d57c798029526125f7698d20c9f6419c557381b0d99ebb55be2ca35a8e819d4
3
+ metadata.gz: 33b663dcc38571d71f8a729f7ed1f8da6ea8a89e1eb6bd49619beac977da2727
4
+ data.tar.gz: af7b47e7e3440372325744d1274e70e41216db757ac224ca2364c1a422b5064a
5
5
  SHA512:
6
- metadata.gz: c5bc56857336297807d32345e8957dfafb714c6f15445da6b69a833b2d41090a4c3a8918ca89f5ec02bdbb593db1ed79996fb3ef5a271aec768358fe26604e67
7
- data.tar.gz: 8fadaa359cdee7cd715e61e89c83ce29e82d471cfb2f00489c365f98078f7d50368aba77542fbe5e3fa6873898803a499c77913fac2dd32e01914cf3bdb7d5d0
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
- > **Current release:** [v0.11.0](https://github.com/OriPekelman/tep/releases/tag/v0.11.0)
11
- > — now on [RubyGems](https://rubygems.org/gems/tep) (`gem install tep`):
12
- > TLS, HTTP caching, connection pooling, PG raise-on-error + embeddings,
13
- > on top of the agentic surface (Auth, Broadcast, Presence, LiveView, MCP).
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
- > **Why Tep exists.** Two complementary goals:
17
- >
18
- > 1. **Exercise Spinel against real-world Ruby code.** Tep is the
19
- > largest pure-Ruby application Spinel compiles end-to-end. Every
20
- > Sinatra idiom, every battery, every demo doubles as a torture
21
- > test for the AOT compiler's codegen + analyzer. Bugs surface
22
- > here, get reduced to minimal repros, and land upstream as PRs
23
- > or issues against [matz/spinel](https://github.com/matz/spinel).
24
- > If something doesn't work in Tep, the bug is usually in Spinel,
25
- > and that's the point.
26
- >
27
- > 2. **Be the harness for [toy][toy].** Toy is Tep's sibling
28
- > project: a machine-learning framework written in pure Ruby and
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 3.4.0; `rv
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: `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`. |
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
- "(__l = Tep.str_hash; #{setters}; tep_view_#{view}(__l, req.ivars))"
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
- "(__l = Tep.str_hash; #{setters}; tep_mustache_#{view}(__l, req.ivars))"
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 -- REMOVE WHEN UPSTREAM LANDS: `e.is_a?(PG::Error)`
72
- # miscompiles at spinel master (whole-program is_a? on a deep
73
- # exception subclass vs an ancestor -- e here is rescued as
74
- # PG::UndefinedTable, a PG::Error subclass, so this is always
75
- # "yes"). Minimal `rescue Sub => e; e.is_a?(Super)` repros
76
- # compile fine; it only trips in the full program. Hardcoded
77
- # to "yes" to keep the example building for the re-pin. See
78
- # matz/spinel#1434, tep#196. Original:
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
- # ============ WORKAROUND -- REMOVE WHEN UPSTREAM LANDS ============
201
- # Hash-conninfo form. The `opts.each` below miscompiles at spinel
202
- # master: `opts` is a String|Hash param, but in this is_a?(String)
203
- # ELSE branch spinel types it String (the is_a?-else narrowing
204
- # gap, matz/spinel#1434) and rejects `opts.each` as String#each
205
- # -- the lone blocker to re-pinning tep onto master (tep#196).
206
- #
207
- # The Hash form is currently UNUSED + untested in tep and toy:
208
- # every PG::Connection.new / PG.connect caller passes a String
209
- # conninfo (AR connects with the String form too). So we stub
210
- # this dead branch to a failed connection to unblock the re-pin.
211
- #
212
- # RESTORE the original kv-pack loop (preserved below) once the
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 < Tep::Handler
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 (spinel's File.read swallows fopen
40
- # errors and returns the empty string -- matches the prior
41
- # sphttp_file_read behaviour).
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
- File.read(path)
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 = File.read(path)
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
@@ -1,3 +1,3 @@
1
1
  module Tep
2
- VERSION = "0.11.5"
2
+ VERSION = "0.11.6"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tep
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.5
4
+ version: 0.11.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ori Pekelman