tep 0.11.3 → 0.11.5
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/Makefile +42 -2
- data/README.md +4 -4
- data/SINATRA_COMPAT.md +20 -20
- data/bin/tep +47 -10
- data/examples/api_gateway/app.rb +1 -1
- data/examples/blog/app.rb +17 -17
- data/examples/chat/app.rb +12 -12
- data/examples/chatbot/README.md +2 -2
- data/examples/chatbot/app.rb +24 -24
- data/examples/llm_gateway/app.rb +4 -4
- data/examples/pg_hello.rb +11 -1
- data/lib/spinel_kit/hex.rb +65 -0
- data/lib/spinel_kit/json.rb +151 -0
- data/lib/spinel_kit/json_decoder.rb +396 -0
- data/lib/{tep/logger.rb → spinel_kit/log.rb} +25 -21
- data/lib/spinel_kit/url.rb +166 -0
- data/lib/tep/auth_bearer_token.rb +6 -6
- data/lib/tep/auth_oauth2.rb +4 -4
- data/lib/tep/broadcast.rb +18 -80
- data/lib/tep/events.rb +37 -37
- data/lib/tep/http.rb +3 -3
- data/lib/tep/job.rb +2 -2
- data/lib/tep/jwt.rb +4 -4
- data/lib/tep/live_view.rb +4 -4
- data/lib/tep/llm.rb +13 -45
- data/lib/tep/mcp.rb +12 -12
- data/lib/tep/multipart.rb +1 -1
- data/lib/tep/net.rb +8 -3
- data/lib/tep/openai_server.rb +102 -94
- data/lib/tep/parser.rb +2 -2
- data/lib/tep/pg.rb +468 -14
- data/lib/tep/presence.rb +33 -329
- data/lib/tep/proxy.rb +7 -7
- data/lib/tep/request.rb +1 -1
- data/lib/tep/response.rb +1 -1
- data/lib/tep/router.rb +1 -1
- data/lib/tep/session.rb +2 -2
- data/lib/tep/version.rb +1 -1
- data/lib/tep.rb +57 -137
- data/spinel-ext.json +6 -0
- data/test/helper.rb +95 -8
- data/test/run_parallel.rb +44 -7
- data/test/test_auth.rb +17 -17
- data/test/test_auth_oauth2.rb +5 -5
- data/test/test_broadcast_pg.rb +1 -0
- data/test/test_http_pool.rb +4 -4
- data/test/test_http_pool_send.rb +3 -3
- data/test/test_json.rb +12 -12
- data/test/test_jwt.rb +4 -4
- data/test/test_live_view.rb +3 -3
- data/test/test_llm.rb +12 -9
- data/test/test_llm_gateway.rb +2 -2
- data/test/test_logger.rb +2 -2
- data/test/test_openai_server.rb +10 -1
- data/test/test_password.rb +3 -3
- data/test/test_pg.rb +1 -0
- data/test/test_presence_pg.rb +1 -0
- data/test/test_real_world.rb +6 -1
- data/test/test_shutdown.rb +40 -0
- metadata +23 -8
- data/lib/tep/json.rb +0 -572
- data/lib/tep/url.rb +0 -161
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0f6e0ace2299c85ad62734b3af3b5b5a704994d2c0769f3151f1741b2c7ec26
|
|
4
|
+
data.tar.gz: 0d57c798029526125f7698d20c9f6419c557381b0d99ebb55be2ca35a8e819d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5bc56857336297807d32345e8957dfafb714c6f15445da6b69a833b2d41090a4c3a8918ca89f5ec02bdbb593db1ed79996fb3ef5a271aec768358fe26604e67
|
|
7
|
+
data.tar.gz: 8fadaa359cdee7cd715e61e89c83ce29e82d471cfb2f00489c365f98078f7d50368aba77542fbe5e3fa6873898803a499c77913fac2dd32e01914cf3bdb7d5d0
|
data/Makefile
CHANGED
|
@@ -34,7 +34,25 @@ TEP_PG_LIBS ?= $(shell \
|
|
|
34
34
|
(pg_config --libdir 2>/dev/null | sed -e 's|^|-L|' ; echo "-lpq") | tr '\n' ' ')
|
|
35
35
|
export TEP_PG_CFLAGS TEP_PG_LIBS
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
# OpenSSL cflags / libs for sphttp.c's outbound-TLS include (#148). On Linux
|
|
38
|
+
# the headers/libs are on the default search path so this is usually empty;
|
|
39
|
+
# on macOS Homebrew keeps openssl@3 keg-only (/opt/homebrew/opt/openssl@3),
|
|
40
|
+
# off the default path, so pkg-config supplies the -I (compile) and -L/-lssl
|
|
41
|
+
# /-lcrypto (link). If pkg-config can't find openssl, fall back to the bare
|
|
42
|
+
# libs and let the include path come from the system default.
|
|
43
|
+
TEP_SPHTTP_CFLAGS ?= $(shell pkg-config --cflags openssl 2>/dev/null)
|
|
44
|
+
TEP_SPHTTP_LIBS ?= $(shell pkg-config --libs openssl 2>/dev/null || echo "-lssl -lcrypto")
|
|
45
|
+
export TEP_SPHTTP_CFLAGS TEP_SPHTTP_LIBS
|
|
46
|
+
|
|
47
|
+
.PHONY: all clean helper hello sinatra_style bench bench-tep bench-sinatra demo test test-parallel spinel-fresh test-pg vendor-examples vendor-spinelkit doctor
|
|
48
|
+
|
|
49
|
+
# Re-sync the vendored SpinelKit lib (lib/spinel_kit/, sig/spinel_kit/) from the
|
|
50
|
+
# upstream checkout. spinel_kit is a published gem; tep depends on its JSON codec
|
|
51
|
+
# + logger. INTERIM vendoring (committed under lib/ so it travels with tep) until
|
|
52
|
+
# spinel-compat gains transitive gem->gem vendoring -- OriPekelman/spinelgems#19.
|
|
53
|
+
# Override the source with SPINELKIT_DIR. See tools/vendor-spinelkit.sh.
|
|
54
|
+
vendor-spinelkit:
|
|
55
|
+
@$(TEP_ROOT)/tools/vendor-spinelkit.sh
|
|
38
56
|
|
|
39
57
|
# Vendor each gem example's Gemfile-declared dependencies via
|
|
40
58
|
# bundler-spinel (spinel-compat vendor, from $(SPINELGEMS)) into
|
|
@@ -65,7 +83,7 @@ all: spinel-fresh helper hello sinatra_style bench
|
|
|
65
83
|
helper: spinel-fresh $(LIB_DIR)/sphttp.o $(LIB_DIR)/tep_sqlite.o $(LIB_DIR)/tep_pg.o
|
|
66
84
|
|
|
67
85
|
$(LIB_DIR)/sphttp.o: $(LIB_DIR)/sphttp.c
|
|
68
|
-
cc -O2 -c $< -o $@
|
|
86
|
+
cc -O2 -c $(TEP_SPHTTP_CFLAGS) $< -o $@
|
|
69
87
|
|
|
70
88
|
$(LIB_DIR)/tep_sqlite.o: $(LIB_DIR)/tep_sqlite.c
|
|
71
89
|
cc -O2 -c $< -o $@
|
|
@@ -96,6 +114,28 @@ test: helper
|
|
|
96
114
|
@pkill -f tep-test 2>/dev/null; true
|
|
97
115
|
ruby test/run_all.rb
|
|
98
116
|
|
|
117
|
+
# `make doctor` -- OPTIONAL, DEV-ONLY inference health check via
|
|
118
|
+
# spinel-dev's `doctor` (compile-probe + emit-rbs untyped scan +
|
|
119
|
+
# value-bisect). Runs on the inlined .tep.rb of a couple of examples
|
|
120
|
+
# (tep apps are FFI, so --no-cruby single-sided). NOT a prerequisite of
|
|
121
|
+
# `all`/`test`/CI -- it self-skips when ../spinel-dev is absent, so tep
|
|
122
|
+
# never gains a hard dependency on the tooling. Run `make all` first to
|
|
123
|
+
# produce the .tep.rb. Override the tool path with DOCTOR=.
|
|
124
|
+
DOCTOR ?= ../spinel-dev/tools/doctor/doctor.sh
|
|
125
|
+
doctor:
|
|
126
|
+
@if [ ! -x "$(DOCTOR)" ]; then \
|
|
127
|
+
echo "make doctor: spinel-dev not found at $(DOCTOR) (optional dev tooling); skipping."; \
|
|
128
|
+
else \
|
|
129
|
+
for tep in examples/.hello.tep.rb examples/.sinatra_style.tep.rb; do \
|
|
130
|
+
if [ -f "$$tep" ]; then \
|
|
131
|
+
echo "== doctor $$tep =="; \
|
|
132
|
+
SPINEL_DIR="$${SPINEL_DIR:-$$HOME/sites/spinel}" "$(DOCTOR)" --no-cruby "$$tep" || true; \
|
|
133
|
+
else \
|
|
134
|
+
echo "make doctor: $$tep not built (run \`make all\`); skipping it."; \
|
|
135
|
+
fi; \
|
|
136
|
+
done; \
|
|
137
|
+
fi
|
|
138
|
+
|
|
99
139
|
# `make test-parallel` -- dev fast loop. Runs each test/test_*.rb in
|
|
100
140
|
# its own process in parallel (sidestepping the harness's "one thread
|
|
101
141
|
# per class" constraint), with per-process port-base allocation. Cuts
|
data/README.md
CHANGED
|
@@ -158,8 +158,8 @@ through Spinel.
|
|
|
158
158
|
| Battery | What it covers |
|
|
159
159
|
|------------------|---|
|
|
160
160
|
| `Tep::SQLite` | libsqlite3 wrapper via a small C shim — exec / prepare / bind / step / col / first_str / first_int. |
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
161
|
+
| `SpinelKit::Json` | encode primitives + flat-key decoder for JSON-over-HTTP. |
|
|
162
|
+
| `SpinelKit::Log` | levelled logger (debug/info/warn/error), stderr or file. |
|
|
163
163
|
| `Tep::Jwt` | HS256 JWT encode / verify / decode. |
|
|
164
164
|
| `Tep::Password` | PBKDF2-SHA256, 200k iters, self-describing storage. |
|
|
165
165
|
| `Tep::Security` | `Cors` (before-filter) + `Headers` (HSTS, nosniff, ...). |
|
|
@@ -216,8 +216,8 @@ lives in [`docs/BATTERIES-DESIGN.md`](docs/BATTERIES-DESIGN.md).
|
|
|
216
216
|
exercising the full pre-agentic battery surface
|
|
217
217
|
(`Tep::Server::Scheduled` + `Tep::Llm` + `Tep::SQLite` +
|
|
218
218
|
`Tep::Streamer` + `Tep::Session` + `Tep::Password` + `Tep::Jwt` +
|
|
219
|
-
`Tep::Security::{Cors,Headers}` + `Tep::Assets` + `
|
|
220
|
-
`Tep::Job` + `
|
|
219
|
+
`Tep::Security::{Cors,Headers}` + `Tep::Assets` + `SpinelKit::Json` +
|
|
220
|
+
`Tep::Job` + `SpinelKit::Log`) in ~1500 lines of Ruby + HTML + CSS + JS.
|
|
221
221
|
- **[`examples/llm_gateway/`](examples/llm_gateway/app.rb)** — the
|
|
222
222
|
`Tep::Proxy` streaming demo. Block-form DSL gateway in front of an
|
|
223
223
|
OpenAI-compatible upstream; pumps SSE token deltas straight through
|
data/SINATRA_COMPAT.md
CHANGED
|
@@ -10,8 +10,8 @@ streaming, regex routes, modular `Sinatra::Base`, ERB. v0.3 added
|
|
|
10
10
|
`send_file 'path'`, `configure { ... }` (incl. `:env`), `__END__`
|
|
11
11
|
inline templates, `pass`, multiple chained `before`/`after`,
|
|
12
12
|
optional path segments, full Rack::Request method surface, ERB
|
|
13
|
-
ivar locals, a Mustache subset, Tep::SQLite,
|
|
14
|
-
|
|
13
|
+
ivar locals, a Mustache subset, Tep::SQLite, SpinelKit::Json,
|
|
14
|
+
SpinelKit::Log, Tep::Jwt, Tep::Password, Tep::Security
|
|
15
15
|
(CORS + secure headers), Tep::Assets (compile-time asset
|
|
16
16
|
bundling), Tep::Scheduler (cooperative fiber scheduler with
|
|
17
17
|
poll(2)-backed `io_wait`), Tep::Shell (popen + small-file
|
|
@@ -62,8 +62,8 @@ shape, lowered by the `websocket '/p' do |ws| ... end` DSL hook).
|
|
|
62
62
|
| **ERB ivar locals (`@name`)** | ✅ 3 | Sinatra-style: `@x = v` in handler / `before` filter, `<%= @x %>` in template. Translator stores on a per-request `req.ivars` String=>String bag; templates take `(locals, ivars)`. Values are `(...).to_s`-coerced on write. |
|
|
63
63
|
| **Mustache (subset)** | ✅ 3 | Build-time compiled; `mustache :name` DSL parallel to `erb :name`. See "Mustache subset" below. |
|
|
64
64
|
| **SQLite (libsqlite3 wrapper)** | ✅ 5 | `Tep::SQLite` class wrapping libsqlite3 via a thin C shim (tep_sqlite.c). Same FFI pattern as sphttp.c -- spinel can't load gem-style native extensions, so we link a static .o instead. See "SQLite" below. |
|
|
65
|
-
| **JSON (subset)** | ✅ 13 | Pure-Ruby `
|
|
66
|
-
| **Logger** | ✅ 3 | `
|
|
65
|
+
| **JSON (subset)** | ✅ 13 | Pure-Ruby `SpinelKit::Json`: encode primitives + flat-key decoder. See "JSON subset" below. |
|
|
66
|
+
| **Logger** | ✅ 3 | `SpinelKit::Log` with debug/info/warn/error levels. stderr by default; `to_file(path)` appends. Format: `[<unix_seconds>] [<level>] <msg>`. |
|
|
67
67
|
| **JWT (HS256)** | ✅ 10 | `Tep::Jwt` -- encode/verify/decode. HS256 only (asymmetric algs would need OpenSSL); `none` deliberately not supported (RFC 8725 §3.1). Tokens verify cleanly against the canonical `jwt` Ruby gem (interop test included). New base64url helpers (`sphttp_b64url_encode/decode`, `sphttp_hmac_sha256_b64url`) ride on top of the existing HMAC-SHA256 used by the session store. |
|
|
68
68
|
| **Password hashing (PBKDF2)** | ✅ 9 | `Tep::Password.hash` / `verify`. PBKDF2-SHA256, 200k iters by default, 16-byte CSPRNG salt. Self-describing storage format (`pbkdf2-sha256$<iters>$<salt>$<derived>`) so iter rotation can land later without breaking old hashes. New `sphttp_pbkdf2_sha256_b64url` + `sphttp_random_b64url` C helpers. (`Klass.hash(plain)` factory shape resolved via spinel #407.) |
|
|
69
69
|
| **CORS + secure headers** | ✅ 4 | `Tep::Security::Cors` (before-filter; configurable origin / verbs / headers / max-age; OPTIONS preflight short-circuits with 204) and `Tep::Security::Headers` (after-filter; `nosniff`, `SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin`, `X-XSS-Protection: 0`, optional HSTS via `set_hsts(seconds)`). |
|
|
@@ -119,9 +119,9 @@ Two flagship examples that put the framework through its paces.
|
|
|
119
119
|
### `examples/blog/`
|
|
120
120
|
|
|
121
121
|
Posts + users persisted in SQLite, web login via sessions +
|
|
122
|
-
`Tep::Password`, JSON API with `
|
|
122
|
+
`Tep::Password`, JSON API with `SpinelKit::Json`, JWT-authed writes via
|
|
123
123
|
`Tep::Jwt`, ERB views with Sinatra-style `@ivar` locals, request
|
|
124
|
-
logging via `
|
|
124
|
+
logging via `SpinelKit::Log`, CORS + secure headers via
|
|
125
125
|
`Tep::Security`. First boot seeds `alice / hunter2` and an intro
|
|
126
126
|
post explaining what tep is.
|
|
127
127
|
|
|
@@ -308,7 +308,7 @@ Constraints:
|
|
|
308
308
|
|
|
309
309
|
## JSON subset
|
|
310
310
|
|
|
311
|
-
`
|
|
311
|
+
`SpinelKit::Json` is a pure-Ruby JSON shim covering the encode + decode
|
|
312
312
|
shapes that JSON-over-HTTP APIs use in practice. It deliberately
|
|
313
313
|
trades full library breadth for spinel-friendly code paths.
|
|
314
314
|
|
|
@@ -316,24 +316,24 @@ trades full library breadth for spinel-friendly code paths.
|
|
|
316
316
|
|
|
317
317
|
```ruby
|
|
318
318
|
# Primitives.
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
SpinelKit::Json.escape(s) # body of a JSON string literal (no quotes)
|
|
320
|
+
SpinelKit::Json.quote(s) # "<escaped s>"
|
|
321
321
|
|
|
322
322
|
# Object building blocks (fixed-arity; compose by concatenation).
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
SpinelKit::Json.encode_pair_str("k", v_string) # "k":"v"
|
|
324
|
+
SpinelKit::Json.encode_pair_int("k", v_int) # "k":N
|
|
325
325
|
|
|
326
326
|
# Build a full object literal:
|
|
327
|
-
"{" +
|
|
328
|
-
|
|
327
|
+
"{" + SpinelKit::Json.encode_pair_str("name", name) + "," +
|
|
328
|
+
SpinelKit::Json.encode_pair_int("age", age) + "}"
|
|
329
329
|
|
|
330
330
|
# Arrays.
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
SpinelKit::Json.from_str_array(["a", "b"]) # ["a","b"]
|
|
332
|
+
SpinelKit::Json.from_int_array([1, 2, 3]) # [1,2,3]
|
|
333
333
|
|
|
334
334
|
# Hashes (String=>String, String=>Int).
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
SpinelKit::Json.from_str_hash({"name" => "alice"}) # {"name":"alice"}
|
|
336
|
+
SpinelKit::Json.from_int_hash({"age" => 30}) # {"age":30}
|
|
337
337
|
```
|
|
338
338
|
|
|
339
339
|
The hash forms `each`-iterate and inline `Json.quote` on the
|
|
@@ -345,9 +345,9 @@ a Hash first.
|
|
|
345
345
|
### Decode (flat-key, top-level only)
|
|
346
346
|
|
|
347
347
|
```ruby
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
348
|
+
SpinelKit::Json.get_str(body, "name") # value of top-level "name", or "" if absent / non-string
|
|
349
|
+
SpinelKit::Json.get_int(body, "age") # 0 if absent / non-numeric
|
|
350
|
+
SpinelKit::Json.has_key?(body, "x") # boolean
|
|
351
351
|
```
|
|
352
352
|
|
|
353
353
|
The hand-rolled state-machine parser walks one `{ "k": <value>, ... }`
|
data/bin/tep
CHANGED
|
@@ -563,7 +563,7 @@ def translate(input_path)
|
|
|
563
563
|
# Tep::MCP emission. For each `mcp_tool 'name', "desc" do ... end`:
|
|
564
564
|
#
|
|
565
565
|
# * TepMCP_Tools.call_<i>(req, args_json) -- static cmeth that
|
|
566
|
-
# parses args out of args_json (via
|
|
566
|
+
# parses args out of args_json (via SpinelKit::Json.get_str/get_int),
|
|
567
567
|
# binds them as locals, and runs the user's on_call body. The
|
|
568
568
|
# body is rewritten the same way route bodies are (req / res /
|
|
569
569
|
# params rewrites). Returns Tep::MCP::Result.
|
|
@@ -620,11 +620,11 @@ def translate(input_path)
|
|
|
620
620
|
end
|
|
621
621
|
t[:params].each do |p|
|
|
622
622
|
if p[:type] == "Integer"
|
|
623
|
-
out << " #{p[:name]} =
|
|
623
|
+
out << " #{p[:name]} = SpinelKit::Json.get_int(args_json, #{p[:name].inspect})"
|
|
624
624
|
elsif p[:type] == "Float"
|
|
625
|
-
out << " #{p[:name]} =
|
|
625
|
+
out << " #{p[:name]} = SpinelKit::Json.get_str(args_json, #{p[:name].inspect}).to_f"
|
|
626
626
|
else
|
|
627
|
-
out << " #{p[:name]} =
|
|
627
|
+
out << " #{p[:name]} = SpinelKit::Json.get_str(args_json, #{p[:name].inspect})"
|
|
628
628
|
end
|
|
629
629
|
end
|
|
630
630
|
out << indent(body, 4)
|
|
@@ -707,8 +707,8 @@ def translate(input_path)
|
|
|
707
707
|
out << " def handle(req, res)"
|
|
708
708
|
out << " res.headers[\"Content-Type\"] = \"application/json\""
|
|
709
709
|
out << " body = req.raw_body"
|
|
710
|
-
out << " method =
|
|
711
|
-
out << " req_id =
|
|
710
|
+
out << " method = SpinelKit::Json.get_str(body, \"method\")"
|
|
711
|
+
out << " req_id = SpinelKit::Json.get_int(body, \"id\")"
|
|
712
712
|
out << " if method == \"initialize\""
|
|
713
713
|
out << " return Tep::MCP.initialize_envelope(req_id, #{server_name.inspect}, #{server_version.inspect})"
|
|
714
714
|
out << " end"
|
|
@@ -724,7 +724,7 @@ def translate(input_path)
|
|
|
724
724
|
out << " end"
|
|
725
725
|
out << " if method == \"tools/call\""
|
|
726
726
|
out << " params = Tep::MCP.nested_extract(body, \"params\")"
|
|
727
|
-
out << " tool_name =
|
|
727
|
+
out << " tool_name = SpinelKit::Json.get_str(params, \"name\")"
|
|
728
728
|
out << " args = Tep::MCP.nested_extract(params, \"arguments\")"
|
|
729
729
|
mcp_tools.each_with_index do |t, i|
|
|
730
730
|
out << " if tool_name == #{t[:name].inspect}"
|
|
@@ -740,7 +740,7 @@ def translate(input_path)
|
|
|
740
740
|
out << " end"
|
|
741
741
|
out << " if method == \"resources/read\""
|
|
742
742
|
out << " res_params = Tep::MCP.nested_extract(body, \"params\")"
|
|
743
|
-
out << " res_uri =
|
|
743
|
+
out << " res_uri = SpinelKit::Json.get_str(res_params, \"uri\")"
|
|
744
744
|
mcp_resources.each_with_index do |rdef, i|
|
|
745
745
|
out << " if res_uri == #{rdef[:name].inspect}"
|
|
746
746
|
out << " c = TepMCP_Resources.read_#{i}(req)"
|
|
@@ -1366,6 +1366,30 @@ def handle_top_call(node, routes, websockets, mcp_tools, mcp_resources, filters,
|
|
|
1366
1366
|
|
|
1367
1367
|
case name
|
|
1368
1368
|
when "require", "require_relative"
|
|
1369
|
+
# tep/pg is OPT-IN (#216): it is NOT part of the wholesale-inlined
|
|
1370
|
+
# core (lib/tep.rb no longer require_relative's it). An app, test, or
|
|
1371
|
+
# example that needs PostgreSQL says `require "tep/pg"` (or, by path,
|
|
1372
|
+
# `require_relative ".../lib/tep/pg"`); we splice lib/tep/pg.rb in
|
|
1373
|
+
# once here. A non-PG app never names it, so the whole pg closure
|
|
1374
|
+
# (the 74 tep_pg_* FFI symbols + libpq) DCEs away. This must run
|
|
1375
|
+
# BEFORE the is_tep_lib no-op below, which would otherwise swallow
|
|
1376
|
+
# any "tep/..." spelling as already-inlined.
|
|
1377
|
+
pg_arg = node.arguments&.arguments&.first
|
|
1378
|
+
if pg_arg.is_a?(Prism::StringNode) &&
|
|
1379
|
+
(pg_arg.unescaped == "tep/pg" || pg_arg.unescaped.end_with?("/tep/pg"))
|
|
1380
|
+
pgpath = File.join(LIB_DIR, "tep", "pg.rb")
|
|
1381
|
+
if inlined_seen && File.file?(pgpath) && !inlined_seen[pgpath]
|
|
1382
|
+
pgtext = inline_require_relative_tree(pgpath, inlined_seen, warnings)
|
|
1383
|
+
# Resolve pg.rb's @TEP_PG_O@ / @TEP_PG_CFLAGS@ placeholders the
|
|
1384
|
+
# same way inlined_tep_library does for the wholesale core -- the
|
|
1385
|
+
# opt-in splice bypasses that pass, so without this the literal
|
|
1386
|
+
# placeholders reach the linker ("ld: cannot find @TEP_PG_O@").
|
|
1387
|
+
tep_ext_subs.each { |placeholder, value| pgtext = pgtext.gsub(placeholder, value) }
|
|
1388
|
+
passthrough << "# --- inlined opt-in require #{pg_arg.unescaped.inspect} (pg.rb) ---"
|
|
1389
|
+
passthrough << pgtext
|
|
1390
|
+
end
|
|
1391
|
+
return
|
|
1392
|
+
end
|
|
1369
1393
|
# tep's own library is inlined wholesale (see inlined_tep_library), so
|
|
1370
1394
|
# `require_relative "tep"` / "tep/..." is a no-op. A plain `require
|
|
1371
1395
|
# "gem"` (by name) is dropped -- spinel has no gem load path. But an
|
|
@@ -1947,7 +1971,11 @@ def tep_ext_subs
|
|
|
1947
1971
|
else
|
|
1948
1972
|
cflags = ENV.fetch(env_var, "")
|
|
1949
1973
|
libs_var = env_var.sub(/_CFLAGS\z/, "_LIBS")
|
|
1950
|
-
|
|
1974
|
+
# Prefer an explicit pkg_config_fallback (e.g. openssl -> "-lssl
|
|
1975
|
+
# -lcrypto"); the bare "-l<pkg_config>" derivation is only right when
|
|
1976
|
+
# the lib name matches the .pc name (libpq -> -lpq), not for openssl.
|
|
1977
|
+
default_libs = entry["pkg_config_fallback"] ||
|
|
1978
|
+
(entry["pkg_config"] ? "-l" + entry["pkg_config"].sub(/\Alib/, "") : "")
|
|
1951
1979
|
libs = ENV.fetch(libs_var, default_libs)
|
|
1952
1980
|
subs[placeholder] = "#{cflags} #{libs}".strip
|
|
1953
1981
|
end
|
|
@@ -1979,7 +2007,16 @@ def resolve_ext_o(entry, entries, env_var)
|
|
|
1979
2007
|
entries.each do |sib|
|
|
1980
2008
|
next unless sib["name"] == entry["name"] && sib["pkg_config"]
|
|
1981
2009
|
pc = `pkg-config --cflags #{sib["pkg_config"]} 2>/dev/null`
|
|
1982
|
-
$?.success?
|
|
2010
|
+
if $?.success?
|
|
2011
|
+
cflags.concat(pc.split)
|
|
2012
|
+
elsif sib["pkg_config_fallback"]
|
|
2013
|
+
# pkg-config can't find a .pc (e.g. openssl on a host without it):
|
|
2014
|
+
# assume the headers are on the default include path -- the
|
|
2015
|
+
# historical behavior before this sibling existed -- and let the
|
|
2016
|
+
# link use the fallback libs. Don't fail an otherwise-fine compile.
|
|
2017
|
+
else
|
|
2018
|
+
missing = sib["pkg_config"]
|
|
2019
|
+
end
|
|
1983
2020
|
end
|
|
1984
2021
|
if missing
|
|
1985
2022
|
return "" if entry["optional"]
|
data/examples/api_gateway/app.rb
CHANGED
|
@@ -22,7 +22,7 @@ require 'sinatra'
|
|
|
22
22
|
UPSTREAM = ENV["UPSTREAM"] || "http://127.0.0.1:8080"
|
|
23
23
|
UPSTREAM_KEY = ENV["UPSTREAM_KEY"] || ""
|
|
24
24
|
GATEWAY_KEY = ENV["GATEWAY_KEY"] || "let-me-in"
|
|
25
|
-
LOGGER =
|
|
25
|
+
LOGGER = SpinelKit::Log.new # stderr; .to_file(path) to redirect
|
|
26
26
|
|
|
27
27
|
# Stand-in for the Auth battery: grant :call_upstream to callers
|
|
28
28
|
# presenting the gateway key. A real app installs Tep::Auth (bearer
|
data/examples/blog/app.rb
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
# - Tep::Password PBKDF2 password hashing
|
|
6
6
|
# - Tep::Jwt JSON API token issue / verify
|
|
7
7
|
# - Sessions web-side login (signed cookie)
|
|
8
|
-
# -
|
|
9
|
-
# -
|
|
8
|
+
# - SpinelKit::Json JSON encode + flat-key decode
|
|
9
|
+
# - SpinelKit::Log request log + auth events
|
|
10
10
|
# - Tep::Security CORS + secure-headers
|
|
11
11
|
# - ERB + @ivar locals public-facing views
|
|
12
12
|
#
|
|
@@ -36,7 +36,7 @@ SEED_PASSWORD = "hunter2"
|
|
|
36
36
|
# at build time for convenience.
|
|
37
37
|
Tep.session_secret = SESSION_SEED
|
|
38
38
|
|
|
39
|
-
LOGGER =
|
|
39
|
+
LOGGER = SpinelKit::Log.new
|
|
40
40
|
LOGGER.set_level("info")
|
|
41
41
|
|
|
42
42
|
CORS = Tep::Security::Cors.new
|
|
@@ -270,9 +270,9 @@ get '/api/posts' do
|
|
|
270
270
|
end
|
|
271
271
|
first = false
|
|
272
272
|
out = out + "{" +
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
SpinelKit::Json.encode_pair_int("id", db.col_int(0)) + "," +
|
|
274
|
+
SpinelKit::Json.encode_pair_str("title", db.col_str(1)) + "," +
|
|
275
|
+
SpinelKit::Json.encode_pair_str("author", db.col_str(2)) + "}"
|
|
276
276
|
end
|
|
277
277
|
db.finalize
|
|
278
278
|
db.close
|
|
@@ -293,16 +293,16 @@ get '/api/posts/:id' do
|
|
|
293
293
|
return "{}"
|
|
294
294
|
end
|
|
295
295
|
"{" +
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
SpinelKit::Json.encode_pair_str("title", title) + "," +
|
|
297
|
+
SpinelKit::Json.encode_pair_str("body", body) + "," +
|
|
298
|
+
SpinelKit::Json.encode_pair_str("author", author) + "}"
|
|
299
299
|
end
|
|
300
300
|
|
|
301
301
|
# Issue a JWT for API access. Same credentials as web login.
|
|
302
302
|
post '/api/token' do
|
|
303
303
|
res.headers["Content-Type"] = "application/json"
|
|
304
|
-
user =
|
|
305
|
-
pwd =
|
|
304
|
+
user = SpinelKit::Json.get_str(req.raw_body, "user")
|
|
305
|
+
pwd = SpinelKit::Json.get_str(req.raw_body, "password")
|
|
306
306
|
|
|
307
307
|
db = Tep::SQLite.new
|
|
308
308
|
db.open(DB_PATH)
|
|
@@ -322,8 +322,8 @@ post '/api/token' do
|
|
|
322
322
|
end
|
|
323
323
|
|
|
324
324
|
payload = "{" +
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
SpinelKit::Json.encode_pair_str("sub", user) + "," +
|
|
326
|
+
SpinelKit::Json.encode_pair_int("exp", Time.now.to_i + 3600) + "}"
|
|
327
327
|
token = Tep::Jwt.encode_hs256(payload, JWT_SECRET)
|
|
328
328
|
LOGGER.info("api token issued: " + user)
|
|
329
329
|
"{\"token\":\"" + token + "\"}"
|
|
@@ -344,10 +344,10 @@ post '/api/posts' do
|
|
|
344
344
|
res.set_status(401)
|
|
345
345
|
return "{\"error\":\"unauthorized\"}"
|
|
346
346
|
end
|
|
347
|
-
user =
|
|
347
|
+
user = SpinelKit::Json.get_str(payload, "sub")
|
|
348
348
|
|
|
349
|
-
title =
|
|
350
|
-
body =
|
|
349
|
+
title = SpinelKit::Json.get_str(req.raw_body, "title")
|
|
350
|
+
body = SpinelKit::Json.get_str(req.raw_body, "body")
|
|
351
351
|
|
|
352
352
|
db = Tep::SQLite.new
|
|
353
353
|
db.open(DB_PATH)
|
|
@@ -363,5 +363,5 @@ post '/api/posts' do
|
|
|
363
363
|
|
|
364
364
|
LOGGER.info("api post created id=" + id.to_s + " by " + user)
|
|
365
365
|
res.set_status(201)
|
|
366
|
-
"{" +
|
|
366
|
+
"{" + SpinelKit::Json.encode_pair_int("id", id) + "}"
|
|
367
367
|
end
|
data/examples/chat/app.rb
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
# - Presence heartbeat table refreshed via POST every
|
|
14
14
|
# few seconds; `who` query lists rows touched
|
|
15
15
|
# in the last 30 s
|
|
16
|
-
# -
|
|
16
|
+
# - SpinelKit::Json wire format for the SSE event payloads
|
|
17
17
|
# and the /who endpoint
|
|
18
|
-
# -
|
|
18
|
+
# - SpinelKit::Log per-connection trace
|
|
19
19
|
# - Tep::Security CORS + secure-headers
|
|
20
20
|
# - ERB + @ivar locals the chat UI page
|
|
21
21
|
#
|
|
@@ -54,7 +54,7 @@ STREAM_MAX = 30 # seconds; streamers self-close after this and
|
|
|
54
54
|
# the client reconnects (so we don't pile up
|
|
55
55
|
# connection-state forever in any one worker)
|
|
56
56
|
|
|
57
|
-
LOGGER =
|
|
57
|
+
LOGGER = SpinelKit::Log.new
|
|
58
58
|
LOGGER.set_level("info")
|
|
59
59
|
|
|
60
60
|
CORS = Tep::Security::Cors.new
|
|
@@ -138,7 +138,7 @@ post '/chat/send' do
|
|
|
138
138
|
db.close
|
|
139
139
|
|
|
140
140
|
LOGGER.info("send id=" + id.to_s + " by " + author + ": " + body)
|
|
141
|
-
"{" +
|
|
141
|
+
"{" + SpinelKit::Json.encode_pair_int("id", id) + "}"
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
post '/chat/heartbeat' do
|
|
@@ -175,8 +175,8 @@ get '/chat/who' do
|
|
|
175
175
|
end
|
|
176
176
|
first = false
|
|
177
177
|
out = out + "{" +
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
SpinelKit::Json.encode_pair_str("user", db.col_str(0)) + "," +
|
|
179
|
+
SpinelKit::Json.encode_pair_int("last_seen", db.col_int(1)) + "}"
|
|
180
180
|
end
|
|
181
181
|
db.finalize
|
|
182
182
|
db.close
|
|
@@ -200,9 +200,9 @@ get '/chat/recent' do
|
|
|
200
200
|
end
|
|
201
201
|
first = false
|
|
202
202
|
out = out + "{" +
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
SpinelKit::Json.encode_pair_int("id", db.col_int(0)) + "," +
|
|
204
|
+
SpinelKit::Json.encode_pair_str("author", db.col_str(1)) + "," +
|
|
205
|
+
SpinelKit::Json.encode_pair_str("body", db.col_str(2)) + "}"
|
|
206
206
|
end
|
|
207
207
|
db.finalize
|
|
208
208
|
db.close
|
|
@@ -245,9 +245,9 @@ class ChatStreamer < Tep::Streamer
|
|
|
245
245
|
author = db.col_str(1)
|
|
246
246
|
body = db.col_str(2)
|
|
247
247
|
line = "data: {" +
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
SpinelKit::Json.encode_pair_int("id", id) + "," +
|
|
249
|
+
SpinelKit::Json.encode_pair_str("author", author) + "," +
|
|
250
|
+
SpinelKit::Json.encode_pair_str("body", body) + "}\n\n"
|
|
251
251
|
out.write(line)
|
|
252
252
|
if id > last_id
|
|
253
253
|
last_id = id
|
data/examples/chatbot/README.md
CHANGED
|
@@ -66,7 +66,7 @@ closed):
|
|
|
66
66
|
| `Tep::Llm` | `Tep::Llm.new(BACKEND_URL).chat(history)` + `.chat_stream(history)` |
|
|
67
67
|
| `Tep::Http` | (under `Tep::Llm`) the actual HTTP transport |
|
|
68
68
|
| `Tep::SQLite` | conversations + messages + app_config tables |
|
|
69
|
-
| `
|
|
69
|
+
| `SpinelKit::Json` | response payloads, manual encoding for nested arrays |
|
|
70
70
|
| `Tep::Password` | first-boot setup + login verify |
|
|
71
71
|
| `Tep::Session` | signed cookie + `authed` flag |
|
|
72
72
|
| `Tep::Assets` | bundled CSS / JS / markdown renderer (served at `/style.css`, `/chat.js`, `/markdown.js` — Tep::Assets paths-relative-to-assets/) |
|
|
@@ -77,7 +77,7 @@ closed):
|
|
|
77
77
|
| `Tep::WebSocket` | live chat over WS (the default `/api/c/ws` route); `Driver#write` is a Streamer-shape alias so `Tep::Llm.chat_stream` drives the socket directly |
|
|
78
78
|
| `Tep::Job` | background conversation-title summarisation |
|
|
79
79
|
| `Tep::Parallel` | multi-backend compare endpoint (sequential dispatch today; the genuine fork fan-out is blocked on [matz/spinel#575](https://github.com/matz/spinel/issues/575)) |
|
|
80
|
-
| `
|
|
80
|
+
| `SpinelKit::Log` | per-request trace to stderr |
|
|
81
81
|
|
|
82
82
|
Phase F is done (closes [tep#11](https://github.com/OriPekelman/tep/issues/11)):
|
|
83
83
|
the JS client opens one WebSocket to `/api/c/ws` and sends
|
data/examples/chatbot/app.rb
CHANGED
|
@@ -59,7 +59,7 @@ HEADERS = Tep::Security::Headers.new
|
|
|
59
59
|
HEADERS.set_hsts(HSTS_SECONDS)
|
|
60
60
|
Tep.after HEADERS
|
|
61
61
|
|
|
62
|
-
LOGGER =
|
|
62
|
+
LOGGER = SpinelKit::Log.new
|
|
63
63
|
LOGGER.set_level("info")
|
|
64
64
|
LOGGER.to_stderr
|
|
65
65
|
|
|
@@ -233,7 +233,7 @@ def conversations_as_json
|
|
|
233
233
|
out = out + ","
|
|
234
234
|
end
|
|
235
235
|
out = out + "{\"id\":" + id.to_s +
|
|
236
|
-
",\"title\":" +
|
|
236
|
+
",\"title\":" + SpinelKit::Json.quote(title) +
|
|
237
237
|
",\"created_at\":" + created.to_s + "}"
|
|
238
238
|
first = false
|
|
239
239
|
end
|
|
@@ -297,7 +297,7 @@ def append_message(conv_id, role, content)
|
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
# Build a JSON envelope for the messages list. Hand-rolled because
|
|
300
|
-
#
|
|
300
|
+
# SpinelKit::Json's flat encoders don't cover nested arrays-of-hashes
|
|
301
301
|
# (same shape Tep::Llm uses internally).
|
|
302
302
|
def messages_as_json(conv_id)
|
|
303
303
|
db = db_open
|
|
@@ -311,8 +311,8 @@ def messages_as_json(conv_id)
|
|
|
311
311
|
if !first
|
|
312
312
|
out = out + ","
|
|
313
313
|
end
|
|
314
|
-
out = out + "{\"role\":" +
|
|
315
|
-
",\"content\":" +
|
|
314
|
+
out = out + "{\"role\":" + SpinelKit::Json.quote(role) +
|
|
315
|
+
",\"content\":" + SpinelKit::Json.quote(content) + "}"
|
|
316
316
|
first = false
|
|
317
317
|
end
|
|
318
318
|
db.finalize
|
|
@@ -415,7 +415,7 @@ class ChatbotFilter < Tep::Filter
|
|
|
415
415
|
def self.deny(res, why)
|
|
416
416
|
res.set_status(401)
|
|
417
417
|
res.headers["Content-Type"] = "application/json"
|
|
418
|
-
res.body = '{"error":"unauthorized","reason":' +
|
|
418
|
+
res.body = '{"error":"unauthorized","reason":' + SpinelKit::Json.quote(why) + '}'
|
|
419
419
|
res.halted = true
|
|
420
420
|
0
|
|
421
421
|
end
|
|
@@ -505,9 +505,9 @@ class JobWorker
|
|
|
505
505
|
if json.length == 0
|
|
506
506
|
return 0
|
|
507
507
|
end
|
|
508
|
-
job_id =
|
|
509
|
-
name =
|
|
510
|
-
arg =
|
|
508
|
+
job_id = SpinelKit::Json.get_int(json, "id")
|
|
509
|
+
name = SpinelKit::Json.get_str(json, "job_name")
|
|
510
|
+
arg = SpinelKit::Json.get_str(json, "arg")
|
|
511
511
|
if name == "TitleJob"
|
|
512
512
|
TitleJob.new.perform(arg)
|
|
513
513
|
Tep::Job.mark_done(DB_PATH, job_id, "")
|
|
@@ -585,7 +585,7 @@ post '/api/token' do
|
|
|
585
585
|
payload_json = '{"sub":"user","iat":' + Time.now.to_i.to_s + '}'
|
|
586
586
|
token = Tep::Jwt.encode_hs256(payload_json, JWT_SECRET)
|
|
587
587
|
res.headers["Content-Type"] = "application/json"
|
|
588
|
-
'{"token":' +
|
|
588
|
+
'{"token":' + SpinelKit::Json.quote(token) + '}'
|
|
589
589
|
end
|
|
590
590
|
|
|
591
591
|
# -------------------------------------------------------------------
|
|
@@ -613,7 +613,7 @@ end
|
|
|
613
613
|
# -------------------------------------------------------------------
|
|
614
614
|
|
|
615
615
|
# Parse the OpenAI request body into a Tep::Llm::Message array.
|
|
616
|
-
# Hand-rolled because
|
|
616
|
+
# Hand-rolled because SpinelKit::Json's flat decoder doesn't dive into
|
|
617
617
|
# the messages-array shape. Walks `"messages":[{"role":"...","content":"..."},...]`
|
|
618
618
|
# and pulls each role/content pair.
|
|
619
619
|
def parse_openai_messages(body)
|
|
@@ -660,10 +660,10 @@ end
|
|
|
660
660
|
def openai_envelope(model, content, stop_reason)
|
|
661
661
|
'{"id":"chatcmpl-tep","object":"chat.completion","created":' +
|
|
662
662
|
Time.now.to_i.to_s +
|
|
663
|
-
',"model":' +
|
|
663
|
+
',"model":' + SpinelKit::Json.quote(model) +
|
|
664
664
|
',"choices":[{"index":0,"message":{"role":"assistant","content":' +
|
|
665
|
-
|
|
666
|
-
'},"finish_reason":' +
|
|
665
|
+
SpinelKit::Json.quote(content) +
|
|
666
|
+
'},"finish_reason":' + SpinelKit::Json.quote(stop_reason) +
|
|
667
667
|
'}]}'
|
|
668
668
|
end
|
|
669
669
|
|
|
@@ -700,7 +700,7 @@ post '/api/v1/chat/completions' do
|
|
|
700
700
|
|
|
701
701
|
# Extract model + stream flag from the JSON body. Model
|
|
702
702
|
# falls back to the chatbot's configured default.
|
|
703
|
-
model =
|
|
703
|
+
model = SpinelKit::Json.get_str(body, "model")
|
|
704
704
|
if model.length == 0
|
|
705
705
|
model = MODEL
|
|
706
706
|
end
|
|
@@ -807,10 +807,10 @@ post '/api/compare' do
|
|
|
807
807
|
if i > 0
|
|
808
808
|
out = out + ","
|
|
809
809
|
end
|
|
810
|
-
out = out + "{\"backend\":" +
|
|
811
|
-
",\"model\":" +
|
|
810
|
+
out = out + "{\"backend\":" + SpinelKit::Json.quote(backend) +
|
|
811
|
+
",\"model\":" + SpinelKit::Json.quote(model) +
|
|
812
812
|
",\"took_s\":" + took.to_s +
|
|
813
|
-
",\"content\":" +
|
|
813
|
+
",\"content\":" + SpinelKit::Json.quote(content) + "}"
|
|
814
814
|
i += 1
|
|
815
815
|
end
|
|
816
816
|
out + "]}"
|
|
@@ -830,8 +830,8 @@ def compare_backends_as_json
|
|
|
830
830
|
if i > 0
|
|
831
831
|
out = out + ","
|
|
832
832
|
end
|
|
833
|
-
out = out + "{\"backend\":" +
|
|
834
|
-
",\"model\":" +
|
|
833
|
+
out = out + "{\"backend\":" + SpinelKit::Json.quote(backend) +
|
|
834
|
+
",\"model\":" + SpinelKit::Json.quote(model) + "}"
|
|
835
835
|
i += 1
|
|
836
836
|
end
|
|
837
837
|
out + "]"
|
|
@@ -961,8 +961,8 @@ end
|
|
|
961
961
|
# keeps sending message frames.
|
|
962
962
|
websocket "/api/c/ws" do |ws|
|
|
963
963
|
on_message do |evt|
|
|
964
|
-
conv_id =
|
|
965
|
-
content =
|
|
964
|
+
conv_id = SpinelKit::Json.get_int(evt.data, "conv_id")
|
|
965
|
+
content = SpinelKit::Json.get_str(evt.data, "content")
|
|
966
966
|
if conv_id > 0 && content.length > 0
|
|
967
967
|
append_message(conv_id, "user", content)
|
|
968
968
|
msgs = conversation_history(conv_id)
|
|
@@ -1019,6 +1019,6 @@ post '/api/send' do
|
|
|
1019
1019
|
end
|
|
1020
1020
|
|
|
1021
1021
|
res.headers["Content-Type"] = "application/json"
|
|
1022
|
-
'{"role":"assistant","content":' +
|
|
1023
|
-
',"stop_reason":' +
|
|
1022
|
+
'{"role":"assistant","content":' + SpinelKit::Json.quote(reply.content) +
|
|
1023
|
+
',"stop_reason":' + SpinelKit::Json.quote(reply.stop_reason) + '}'
|
|
1024
1024
|
end
|