tep 0.11.5 → 0.11.7
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 +59 -13
- data/examples/blog/app.rb +1 -0
- data/examples/experiments/app.rb +3 -1
- data/examples/pg_hello.rb +9 -8
- data/lib/tep/app.rb +1 -0
- data/lib/tep/auth_bearer_token.rb +1 -0
- data/lib/tep/auth_session_cookie.rb +6 -0
- data/lib/tep/cache.rb +3 -0
- data/lib/tep/http.rb +7 -5
- data/lib/tep/openai_server.rb +3 -0
- data/lib/tep/parser.rb +4 -1
- data/lib/tep/pg.rb +29 -28
- data/lib/tep/proxy.rb +2 -2
- data/lib/tep/request.rb +16 -6
- data/lib/tep/server.rb +5 -1
- data/lib/tep/server_scheduled.rb +8 -5
- data/lib/tep/session.rb +9 -1
- data/lib/tep/shell.rb +17 -6
- data/lib/tep/version.rb +1 -1
- data/lib/tep/websocket/handshake.rb +7 -1
- data/lib/tep.rb +22 -0
- data/test/differential/10_semantics.rb +56 -0
- data/test/differential/11_matrix.rb +63 -0
- data/test/differential/Gemfile +14 -0
- data/test/differential/runner.rb +250 -0
- data/test/test_broadcast_pg.rb +6 -2
- data/test/test_live_view.rb +3 -1
- data/test/test_mcp.rb +3 -1
- data/test/test_pg.rb +21 -2
- data/test/test_real_world.rb +8 -3
- data/test/test_request_methods.rb +7 -1
- data/test/test_unsupported.rb +47 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f69c2e25a18f0ad96cc6a765e9655cc130c35ecc90fe006715409868c8e69e72
|
|
4
|
+
data.tar.gz: bfe36187e4d9565b03c58b37ac1e0243f96dbe3b888752983acc6d6f87595fc7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65c8b19acd15c4548a98a3db081e5f0c9d5b73068b8e99744e9064a35f1a4cdc920aa0a447061d708cd889155c5034efc6303532abee38869b193b5a1d10241d
|
|
7
|
+
data.tar.gz: 6307c6fefde2da3b7a3e67480d1c59ab051527c62796d03971fe25e1596c0fbee7271ea9d9d531b458ec310b99758e9d81b075d1f4c139d9500c25afc677a71a
|
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.7](https://github.com/OriPekelman/tep/releases/tag/v0.11.7)
|
|
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
|
@@ -86,7 +86,14 @@ def fatal(msg)
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
# Build the translated Ruby source from a Sinatra-style file.
|
|
89
|
-
|
|
89
|
+
#
|
|
90
|
+
# Strict by default (mirror condition 3, docs/mirrors/sinatra.md):
|
|
91
|
+
# any construct the translator would drop -- Rack `use`, `helpers`,
|
|
92
|
+
# unknown `set` keys, on_stop, malformed DSL blocks, missing views --
|
|
93
|
+
# is a fatal error, because building anyway produces a binary that
|
|
94
|
+
# silently diverges from what the same source does under real
|
|
95
|
+
# sinatra. `--lax` (or TEP_LAX=1) restores warn-and-continue.
|
|
96
|
+
def translate(input_path, lax: ENV["TEP_LAX"] == "1")
|
|
90
97
|
raw_source = File.read(input_path)
|
|
91
98
|
|
|
92
99
|
# Split off `__END__` inline templates. Sinatra's convention is
|
|
@@ -909,7 +916,19 @@ def translate(input_path)
|
|
|
909
916
|
RB
|
|
910
917
|
out << ""
|
|
911
918
|
|
|
912
|
-
warnings.
|
|
919
|
+
if warnings.any?
|
|
920
|
+
if lax
|
|
921
|
+
warnings.each { |w| warn "tep: #{w}" }
|
|
922
|
+
warn "tep: --lax: built with #{warnings.length} dropped construct(s); " \
|
|
923
|
+
"behaviour diverges from real sinatra where they applied"
|
|
924
|
+
else
|
|
925
|
+
warnings.each { |w| warn "tep: error: #{w}" }
|
|
926
|
+
fatal "#{warnings.length} unsupported construct(s) -- refusing to build " \
|
|
927
|
+
"a binary that silently diverges (docs/mirrors/sinatra.md, " \
|
|
928
|
+
"condition 3). Re-run with --lax (or TEP_LAX=1) to drop them " \
|
|
929
|
+
"with a warning instead."
|
|
930
|
+
end
|
|
931
|
+
end
|
|
913
932
|
|
|
914
933
|
out.join("\n")
|
|
915
934
|
end
|
|
@@ -1596,8 +1615,12 @@ def rewrite_block(src, force_string: true)
|
|
|
1596
1615
|
# `headers["X"] = "y"` -> `res.headers[...]` when not already qualified
|
|
1597
1616
|
s = s.gsub(/(?<![\w.])headers\[/, "res.headers[")
|
|
1598
1617
|
|
|
1599
|
-
# `cookies["k"]` -> `
|
|
1600
|
-
|
|
1618
|
+
# `cookies["k"] = v` (write) -> direct hash write; `cookies["k"]`
|
|
1619
|
+
# (read) -> Tep.cookie(req, "k"), which nil-guards the miss: the
|
|
1620
|
+
# documented cookies[] contract is ""-on-miss (test_cookies pins
|
|
1621
|
+
# it), but the underlying hash reads nil for a missing key.
|
|
1622
|
+
s = s.gsub(/(?<![\w.])cookies\[([^\]]*)\]\s*=(?![=~])/, 'req.cookies[\1] =')
|
|
1623
|
+
s = s.gsub(/(?<![\w.])cookies\[([^\]]*)\]/, 'Tep.cookie(req, \1)')
|
|
1601
1624
|
|
|
1602
1625
|
# `erb :name` -> `tep_view_name(Tep.str_hash, req.ivars)`
|
|
1603
1626
|
# `erb :name, locals: {a: "b", c: "d"}` -> build a String=>String
|
|
@@ -1610,7 +1633,14 @@ def rewrite_block(src, force_string: true)
|
|
|
1610
1633
|
# are handled separately so they can contain commas or spaces.
|
|
1611
1634
|
pairs = $2.scan(/(\w+):\s*("[^"]*"|'[^']*'|[^,}]+?)\s*(?=,|\z)/)
|
|
1612
1635
|
setters = pairs.map { |k, v| %{__l["#{k}"] = (#{v.strip}).to_s} }.join("; ")
|
|
1613
|
-
|
|
1636
|
+
# Build the locals hash INSIDE the first argument (a sequence
|
|
1637
|
+
# expression that returns __l), rather than `(__l = ...; render(__l))`
|
|
1638
|
+
# with __l a bare arg. In the latter shape spinel (recent master)
|
|
1639
|
+
# hoists the `render`'s arg temp -- `_t = __l` -- above the in-sequence
|
|
1640
|
+
# `__l = str_hash()` assignment, so render receives the pre-assignment
|
|
1641
|
+
# (empty) hash and every interpolated value blanks (matz/spinel#1478).
|
|
1642
|
+
# Keeping the assignment inside the arg evaluation defeats the hoist.
|
|
1643
|
+
"tep_view_#{view}((__l = Tep.str_hash; #{setters}; __l), req.ivars)"
|
|
1614
1644
|
end
|
|
1615
1645
|
s = s.gsub(/(?<![\w.])erb\s+:(\w+)/, 'tep_view_\1(Tep.str_hash, req.ivars)')
|
|
1616
1646
|
|
|
@@ -1619,7 +1649,8 @@ def rewrite_block(src, force_string: true)
|
|
|
1619
1649
|
view = $1
|
|
1620
1650
|
pairs = $2.scan(/(\w+):\s*("[^"]*"|'[^']*'|[^,}]+?)\s*(?=,|\z)/)
|
|
1621
1651
|
setters = pairs.map { |k, v| %{__l["#{k}"] = (#{v.strip}).to_s} }.join("; ")
|
|
1622
|
-
|
|
1652
|
+
# Same arg-hoist avoidance as the erb form above (matz/spinel#1478).
|
|
1653
|
+
"tep_mustache_#{view}((__l = Tep.str_hash; #{setters}; __l), req.ivars)"
|
|
1623
1654
|
end
|
|
1624
1655
|
s = s.gsub(/(?<![\w.])mustache\s+:(\w+)/, 'tep_mustache_\1(Tep.str_hash, req.ivars)')
|
|
1625
1656
|
|
|
@@ -1689,6 +1720,12 @@ def rewrite_block(src, force_string: true)
|
|
|
1689
1720
|
# to string keys.
|
|
1690
1721
|
s = s.gsub(/(req\.params)\[:(\w+)\]/, '\1["\2"]')
|
|
1691
1722
|
|
|
1723
|
+
# Read form -> Tep.param(req, k), which nil-guards the miss: tep's
|
|
1724
|
+
# documented params[] contract is ""-on-miss (test_params pins it;
|
|
1725
|
+
# ledgered as a sinatra divergence -- sinatra reads nil). Writes
|
|
1726
|
+
# (assignment) stay direct hash writes.
|
|
1727
|
+
s = s.gsub(/req\.params\[([^\]]*)\](?!\s*=[^=~])/, 'Tep.param(req, \1)')
|
|
1728
|
+
|
|
1692
1729
|
# `"#{params['x']}"` interpolation works fine after the params rewrite.
|
|
1693
1730
|
|
|
1694
1731
|
# String-coerce the last expression of the block body. Spinel's
|
|
@@ -2168,23 +2205,25 @@ def cmd_build(args)
|
|
|
2168
2205
|
input = nil
|
|
2169
2206
|
out_path = nil
|
|
2170
2207
|
c_only = false
|
|
2208
|
+
lax = ENV["TEP_LAX"] == "1"
|
|
2171
2209
|
i = 0
|
|
2172
2210
|
while i < args.length
|
|
2173
2211
|
a = args[i]
|
|
2174
2212
|
case a
|
|
2175
2213
|
when "-o" then out_path = args[i + 1]; i += 2
|
|
2176
2214
|
when "-c" then c_only = true; i += 1
|
|
2215
|
+
when "--lax" then lax = true; i += 1
|
|
2177
2216
|
when /^-/ then fatal "unknown option: #{a}"
|
|
2178
2217
|
else
|
|
2179
2218
|
input ||= a
|
|
2180
2219
|
i += 1
|
|
2181
2220
|
end
|
|
2182
2221
|
end
|
|
2183
|
-
fatal "usage: tep build app.rb [-o out] [-c]" unless input
|
|
2222
|
+
fatal "usage: tep build app.rb [-o out] [-c] [--lax]" unless input
|
|
2184
2223
|
|
|
2185
2224
|
base = File.basename(input, ".rb")
|
|
2186
2225
|
out_path ||= File.join(File.dirname(input), base)
|
|
2187
|
-
translated = translate(input)
|
|
2226
|
+
translated = translate(input, lax: lax)
|
|
2188
2227
|
|
|
2189
2228
|
# Spinel's require_relative resolves against the source file's
|
|
2190
2229
|
# directory and chokes on absolute paths. Drop the generated file
|
|
@@ -2222,8 +2261,10 @@ def cmd_run(args)
|
|
|
2222
2261
|
end
|
|
2223
2262
|
|
|
2224
2263
|
def cmd_translate(args)
|
|
2225
|
-
|
|
2226
|
-
|
|
2264
|
+
lax = ENV["TEP_LAX"] == "1"
|
|
2265
|
+
lax = true if args.delete("--lax")
|
|
2266
|
+
input = args.first or fatal "usage: tep translate app.rb [--lax]"
|
|
2267
|
+
puts translate(input, lax: lax)
|
|
2227
2268
|
end
|
|
2228
2269
|
|
|
2229
2270
|
case ARGV.shift
|
|
@@ -2235,9 +2276,14 @@ when nil, "-h", "--help"
|
|
|
2235
2276
|
tep -- Sinatra-flavoured framework that compiles to a native binary.
|
|
2236
2277
|
|
|
2237
2278
|
Usage:
|
|
2238
|
-
tep build app.rb [-o out] [-c]
|
|
2239
|
-
tep run app.rb [-p PORT] [-w N]
|
|
2240
|
-
tep translate app.rb
|
|
2279
|
+
tep build app.rb [-o out] [-c] [--lax] translate + spinel-compile
|
|
2280
|
+
tep run app.rb [-p PORT] [-w N] build then exec
|
|
2281
|
+
tep translate app.rb [--lax] print the translated Ruby
|
|
2282
|
+
|
|
2283
|
+
Strict by default: source constructs tep would have to drop
|
|
2284
|
+
(Rack `use`, `helpers do`, unknown `set` keys, ...) fail the
|
|
2285
|
+
build rather than silently diverging from real sinatra.
|
|
2286
|
+
`--lax` (or TEP_LAX=1) downgrades them to warnings.
|
|
2241
2287
|
|
|
2242
2288
|
Source compatibility: top-level `get '/path' do ... end`,
|
|
2243
2289
|
`post`, `put`, `patch`, `delete`, `before`, `after`, `not_found`,
|
data/examples/blog/app.rb
CHANGED
|
@@ -332,6 +332,7 @@ end
|
|
|
332
332
|
post '/api/posts' do
|
|
333
333
|
res.headers["Content-Type"] = "application/json"
|
|
334
334
|
auth = req.req_headers["authorization"]
|
|
335
|
+
auth = "" if auth.nil? # missing header reads as nil (tep#235)
|
|
335
336
|
bearer = ""
|
|
336
337
|
if auth.length > 7 && auth[0, 7] == "Bearer "
|
|
337
338
|
bearer = auth[7, auth.length - 7]
|
data/examples/experiments/app.rb
CHANGED
|
@@ -218,7 +218,9 @@ end
|
|
|
218
218
|
# baked in. Here, accept an X-Demo-Cap-Run header as the
|
|
219
219
|
# capability source so humans can drive the demo with curl.
|
|
220
220
|
before do
|
|
221
|
-
|
|
221
|
+
cap = req.req_headers["x-demo-cap-run"]
|
|
222
|
+
cap = "" if cap.nil? # missing header reads as nil (tep#235)
|
|
223
|
+
if cap.length > 0
|
|
222
224
|
req.identity = Tep::Identity.new(
|
|
223
225
|
"user:demo", nil, [:run_experiments])
|
|
224
226
|
end
|
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/app.rb
CHANGED
|
@@ -84,12 +84,16 @@ module Tep
|
|
|
84
84
|
# if the session has no identity (no prior #set call, or after
|
|
85
85
|
# #clear) or the stored identity is expired.
|
|
86
86
|
def self.try(req)
|
|
87
|
+
# Session#get on a missing key reads as nil (sinatra-parity Hash
|
|
88
|
+
# semantics; same class as tep#235) -- guard before String calls.
|
|
87
89
|
sub = req.session.get("identity_sub")
|
|
90
|
+
sub = "" if sub.nil?
|
|
88
91
|
if sub.length == 0
|
|
89
92
|
return nil
|
|
90
93
|
end
|
|
91
94
|
|
|
92
95
|
exp_str = req.session.get("identity_exp")
|
|
96
|
+
exp_str = "" if exp_str.nil?
|
|
93
97
|
if exp_str.length > 0
|
|
94
98
|
exp = exp_str.to_i
|
|
95
99
|
if exp > 0 && Time.now.to_i >= exp
|
|
@@ -98,9 +102,11 @@ module Tep
|
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
caps_str = req.session.get("identity_caps")
|
|
105
|
+
caps_str = "" if caps_str.nil?
|
|
101
106
|
caps = Tep::AuthBearerToken.parse_caps(caps_str)
|
|
102
107
|
|
|
103
108
|
delegate_str = req.session.get("identity_delegate")
|
|
109
|
+
delegate_str = "" if delegate_str.nil?
|
|
104
110
|
delegation = Tep::AuthBearerToken.parse_delegate(delegate_str)
|
|
105
111
|
|
|
106
112
|
Tep::Identity.new(sub, delegation, caps)
|
data/lib/tep/cache.rb
CHANGED
|
@@ -18,8 +18,10 @@ module Tep
|
|
|
18
18
|
# tag is matched as a substring so a comma-separated list of tags
|
|
19
19
|
# in If-None-Match works.
|
|
20
20
|
etag = res.headers["ETag"]
|
|
21
|
+
etag = "" if etag.nil?
|
|
21
22
|
if etag.length > 0
|
|
22
23
|
inm = req.headers["if-none-match"]
|
|
24
|
+
inm = "" if inm.nil?
|
|
23
25
|
if inm.length > 0
|
|
24
26
|
if inm == "*"
|
|
25
27
|
return true
|
|
@@ -35,6 +37,7 @@ module Tep
|
|
|
35
37
|
lm = res.lastmod_epoch
|
|
36
38
|
if lm > 0
|
|
37
39
|
ims = req.headers["if-modified-since"]
|
|
40
|
+
ims = "" if ims.nil?
|
|
38
41
|
if ims.length > 0
|
|
39
42
|
ims_epoch = Sock.sphttp_parse_http_date(ims)
|
|
40
43
|
if ims_epoch >= 0 && lm <= ims_epoch
|
data/lib/tep/http.rb
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
# plumbing (sphttp_connect, sphttp_set_nonblock, sphttp_recv_*); the
|
|
10
10
|
# missing piece is the HTTP/1.0 client on top.
|
|
11
11
|
#
|
|
12
|
-
# Scope
|
|
13
|
-
#
|
|
14
|
-
# * **
|
|
15
|
-
#
|
|
12
|
+
# Scope
|
|
13
|
+
# -----
|
|
14
|
+
# * **http + https** -- outbound TLS landed in #150
|
|
15
|
+
# (Sock.sphttp_connect_tls; non-blocking handshake under the
|
|
16
|
+
# scheduled server). Talk to internal services, the local Ollama
|
|
17
|
+
# API, vLLM, your own tep-backed sidecars, or external https APIs.
|
|
16
18
|
# * **HTTP/1.0 + Connection: close** -- one socket per request,
|
|
17
19
|
# read until EOF. No keep-alive, no pipelining.
|
|
18
20
|
# * **No chunked-transfer reading** -- assumes Content-Length or
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
#
|
|
25
27
|
# These limits cover the dashboard's needs (talking to local
|
|
26
28
|
# inference backends) and the bulk of "hit an internal API"
|
|
27
|
-
# workloads.
|
|
29
|
+
# workloads. Keep-alive + chunked reading land as a v2 surface.
|
|
28
30
|
#
|
|
29
31
|
# API shape
|
|
30
32
|
# ---------
|
data/lib/tep/openai_server.rb
CHANGED
|
@@ -141,7 +141,10 @@ module Tep
|
|
|
141
141
|
def self.serve!(events_jsonl = "")
|
|
142
142
|
events = Tep::Events.new(events_jsonl)
|
|
143
143
|
Tep::APP.set_openai_events(events)
|
|
144
|
+
# ENV[] reads nil for an unset variable (CRuby semantics) --
|
|
145
|
+
# guard before String calls (same class as tep#235).
|
|
144
146
|
host = ENV["HOSTNAME"]
|
|
147
|
+
host = "" if host.nil?
|
|
145
148
|
if host.length == 0
|
|
146
149
|
host = "tep"
|
|
147
150
|
end
|
data/lib/tep/parser.rb
CHANGED
|
@@ -55,8 +55,11 @@ module Tep
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Parse Cookie header into req.cookies. Format: "k=v; k2=v2; ...".
|
|
58
|
-
# Whitespace around `;` is allowed and stripped.
|
|
58
|
+
# Whitespace around `;` is allowed and stripped. A request with no
|
|
59
|
+
# Cookie header reads as nil (don't call methods on it: spinel's
|
|
60
|
+
# unresolved-call gate raises NoMethodError, same as CRuby).
|
|
59
61
|
cookie_blob = req.req_headers["cookie"]
|
|
62
|
+
cookie_blob = "" if cookie_blob.nil?
|
|
60
63
|
if cookie_blob.length > 0
|
|
61
64
|
cookie_blob.split(";").each do |pair|
|
|
62
65
|
eq = Tep.str_find(pair, "=", 0)
|
data/lib/tep/pg.rb
CHANGED
|
@@ -190,6 +190,16 @@ module PG
|
|
|
190
190
|
@last_sqlstate = ""
|
|
191
191
|
@last_error_message = ""
|
|
192
192
|
@last_result_rh = -1
|
|
193
|
+
# Never-connected sentinel: PG::Connection.new("") is the
|
|
194
|
+
# type-seeding shape (see the _tep_seed_pg_* block). Exec on a
|
|
195
|
+
# seed conn returns the empty-Result sentinel instead of
|
|
196
|
+
# raising -- a REAL failed connect (non-empty conninfo) still
|
|
197
|
+
# raises PG::UnableToSend on first exec, per the documented
|
|
198
|
+
# non-raising-connect / raising-exec contract.
|
|
199
|
+
@seed_conn = false
|
|
200
|
+
if opts.is_a?(String) && opts.length == 0
|
|
201
|
+
@seed_conn = true
|
|
202
|
+
end
|
|
193
203
|
if opts.is_a?(String)
|
|
194
204
|
if Tep::Scheduler.scheduled_context?
|
|
195
205
|
h = Connection.async_connect(opts)
|
|
@@ -197,34 +207,19 @@ module PG
|
|
|
197
207
|
h = Pg.tep_pg_connect(opts)
|
|
198
208
|
end
|
|
199
209
|
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
|
-
# =================================================================
|
|
210
|
+
# Hash-conninfo form: pack the key/value pairs into NUL-delimited
|
|
211
|
+
# buffers for the C shim. (`opts` narrows to Hash in this
|
|
212
|
+
# is_a?(String) ELSE branch -- the narrowing gap that blocked the
|
|
213
|
+
# re-pin, matz/spinel#1434, is fixed as of the SPINEL_PIN bump.)
|
|
214
|
+
keys = ""
|
|
215
|
+
vals = ""
|
|
216
|
+
n = 0
|
|
217
|
+
opts.each do |k, v|
|
|
218
|
+
keys = keys + k + "\0"
|
|
219
|
+
vals = vals + v + "\0"
|
|
220
|
+
n += 1
|
|
221
|
+
end
|
|
222
|
+
h = Pg.tep_pg_connect_kv(keys, vals, n)
|
|
228
223
|
end
|
|
229
224
|
if h < 0
|
|
230
225
|
# Slot 0 holds the most recent connect-failure error message
|
|
@@ -397,6 +392,9 @@ module PG
|
|
|
397
392
|
Pg.tep_pg_set_nonblocking(@pgh, 1)
|
|
398
393
|
ok = Pg.tep_pg_send_query(@pgh, sql)
|
|
399
394
|
if ok != 1
|
|
395
|
+
if @seed_conn
|
|
396
|
+
return PG::Result.new(-1)
|
|
397
|
+
end
|
|
400
398
|
Connection.raise_send_failure(self)
|
|
401
399
|
end
|
|
402
400
|
Connection.drain_send(@pgh)
|
|
@@ -436,6 +434,9 @@ module PG
|
|
|
436
434
|
Pg.tep_pg_set_nonblocking(@pgh, 1)
|
|
437
435
|
ok = Pg.tep_pg_send_query_params(@pgh, sql)
|
|
438
436
|
if ok != 1
|
|
437
|
+
if @seed_conn
|
|
438
|
+
return PG::Result.new(-1)
|
|
439
|
+
end
|
|
439
440
|
Connection.raise_send_failure(self)
|
|
440
441
|
end
|
|
441
442
|
Connection.drain_send(@pgh)
|
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/request.rb
CHANGED
|
@@ -51,11 +51,16 @@ module Tep
|
|
|
51
51
|
def headers; @req_headers; end
|
|
52
52
|
def body; @raw_body; end
|
|
53
53
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
54
|
+
# A missing header reads as nil (CRuby Hash semantics; spinel's
|
|
55
|
+
# unresolved-call gate raises NoMethodError on nil receivers, same
|
|
56
|
+
# as CRuby would). Guard before calling String methods; an empty
|
|
57
|
+
# header and no header both mean "use HTTP/1.1 default behaviour".
|
|
58
|
+
# NOTE: guard with `x = "" if x.nil?`, not `x || ""` -- the ||-form
|
|
59
|
+
# mis-compiles at spinel pin f6d5eef (blanks the hit value).
|
|
57
60
|
def keep_alive?
|
|
58
|
-
lc = @req_headers["connection"]
|
|
61
|
+
lc = @req_headers["connection"]
|
|
62
|
+
lc = "" if lc.nil?
|
|
63
|
+
lc = lc.downcase
|
|
59
64
|
if lc == "close"
|
|
60
65
|
return false
|
|
61
66
|
end
|
|
@@ -70,7 +75,9 @@ module Tep
|
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
def form?
|
|
73
|
-
@req_headers["content-type"]
|
|
78
|
+
ct = @req_headers["content-type"]
|
|
79
|
+
ct = "" if ct.nil?
|
|
80
|
+
ct.downcase.start_with?("application/x-www-form-urlencoded")
|
|
74
81
|
end
|
|
75
82
|
|
|
76
83
|
# True when the request body is a multipart/form-data submission
|
|
@@ -78,7 +85,9 @@ module Tep
|
|
|
78
85
|
# or carrying file inputs). Tep::Multipart.parse handles the
|
|
79
86
|
# text fields; file-upload parts are skipped in v1.
|
|
80
87
|
def multipart?
|
|
81
|
-
@req_headers["content-type"]
|
|
88
|
+
ct = @req_headers["content-type"]
|
|
89
|
+
ct = "" if ct.nil?
|
|
90
|
+
ct.downcase.start_with?("multipart/form-data")
|
|
82
91
|
end
|
|
83
92
|
|
|
84
93
|
# ---- Rack::Request-style accessors (reads only, no .ip yet) ----
|
|
@@ -99,6 +108,7 @@ module Tep
|
|
|
99
108
|
# proxy sets.
|
|
100
109
|
def scheme
|
|
101
110
|
proto = @req_headers["x-forwarded-proto"]
|
|
111
|
+
proto = "" if proto.nil?
|
|
102
112
|
if proto.length > 0
|
|
103
113
|
return proto.downcase
|
|
104
114
|
end
|
data/lib/tep/server.rb
CHANGED
|
@@ -231,7 +231,11 @@ module Tep
|
|
|
231
231
|
res.set_body("")
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
# Default Content-Type even on empty bodies (redirects!) --
|
|
235
|
+
# sinatra/Rack parity, caught by the differential oracle
|
|
236
|
+
# (test/differential/runner.rb). 204/304 are the exception:
|
|
237
|
+
# Rack strips entity headers there, so we don't default one in.
|
|
238
|
+
if !res.headers.key?("Content-Type") && res.status != 204 && res.status != 304
|
|
235
239
|
res.headers["Content-Type"] = "text/html; charset=utf-8"
|
|
236
240
|
end
|
|
237
241
|
res.headers["Content-Length"] = res.body.length.to_s
|
data/lib/tep/server_scheduled.rb
CHANGED
|
@@ -327,11 +327,14 @@ module Tep
|
|
|
327
327
|
res.file_path = ""
|
|
328
328
|
end
|
|
329
329
|
|
|
330
|
-
# Default Content-Type for inline-body responses
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
|
|
330
|
+
# Default Content-Type for inline-body responses -- including
|
|
331
|
+
# empty ones (redirects), for sinatra/Rack parity (differential
|
|
332
|
+
# oracle finding; matches Tep::Server). 204/304 excepted: Rack
|
|
333
|
+
# strips entity headers there. Without a Content-Type, the
|
|
334
|
+
# Security::Headers nosniff default leaves the browser refusing
|
|
335
|
+
# to interpret an erb response as HTML.
|
|
336
|
+
if res.file_path.length == 0 && !res.headers.key?("Content-Type") &&
|
|
337
|
+
res.status != 204 && res.status != 304
|
|
335
338
|
res.headers["Content-Type"] = "text/html; charset=utf-8"
|
|
336
339
|
end
|
|
337
340
|
reason = Tep.reason(res.status)
|
data/lib/tep/session.rb
CHANGED
|
@@ -24,7 +24,15 @@ module Tep
|
|
|
24
24
|
# exposes only named methods; the translator rewrites
|
|
25
25
|
# `session[k] = v` to `session.set(k, v)` and `session[k]` to
|
|
26
26
|
# `session.get(k)` for source compatibility with Sinatra.
|
|
27
|
-
|
|
27
|
+
# A missing key reads as "" -- the documented `session[k]` DSL
|
|
28
|
+
# contract (test_sessions pins it; a deliberate divergence from
|
|
29
|
+
# sinatra's nil-on-miss, ledgered in docs/mirrors/sinatra.md).
|
|
30
|
+
# The raw @data hash reads nil for a miss.
|
|
31
|
+
def get(k)
|
|
32
|
+
v = @data[k]
|
|
33
|
+
v = "" if v.nil?
|
|
34
|
+
v
|
|
35
|
+
end
|
|
28
36
|
def set(k, v); @data[k] = v; @dirty = true; end
|
|
29
37
|
def has?(k); @data.key?(k); end
|
|
30
38
|
def length; @data.length; end
|