tina4ruby 3.13.39 → 3.13.40
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/lib/tina4/auth.rb +5 -1
- data/lib/tina4/dev_admin.rb +79 -12
- data/lib/tina4/docstore.rb +753 -0
- data/lib/tina4/env.rb +7 -2
- data/lib/tina4/mcp.rb +92 -20
- data/lib/tina4/rack_app.rb +16 -4
- data/lib/tina4/swagger.rb +166 -32
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bb742a8cdfa909ee27c95cce703f14f678f03f93b764aea62501b4afde78e91
|
|
4
|
+
data.tar.gz: 3e0da501f8911d1df438ecebdab6b2f8ef67be7b66d39927672c23f089b351c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f6998cc599c7ebae85a3a0020b8146721ca653a84f7f65635226072781aac2ad41ccec3cf3a749063960e3f1bae47c3e3eca97fe58375d6c01c0a5df7359487
|
|
7
|
+
data.tar.gz: ce0653713c6f9e0659b5f26a6068690db7915ccbc8cdeb87ca54c8cfe609c2b700f6e90a5f6ca90b8f6c198d33edbcea70599bb050855259504fd831f16b90f6
|
data/lib/tina4/auth.rb
CHANGED
|
@@ -15,7 +15,11 @@ module Tina4
|
|
|
15
15
|
BLANK_SECRET_WARNING =
|
|
16
16
|
"Auth: TINA4_SECRET is not set — JWT signing is insecure. Set TINA4_SECRET " \
|
|
17
17
|
"to a random value (e.g. `openssl rand -hex 32`) in your environment or " \
|
|
18
|
-
".env before serving traffic."
|
|
18
|
+
".env before serving traffic. " \
|
|
19
|
+
"For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated " \
|
|
20
|
+
"automatically into .env.local (gitignored). Seeing this warning means the " \
|
|
21
|
+
"run was NOT detected as dev - typically a container or CI without " \
|
|
22
|
+
"TINA4_DEBUG set, or TINA4_ENV=production."
|
|
19
23
|
|
|
20
24
|
class << self
|
|
21
25
|
def setup(root_dir = Dir.pwd)
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -644,22 +644,25 @@ module Tina4
|
|
|
644
644
|
json_response(deps_install(body))
|
|
645
645
|
when ["GET", "/__dev/api/git/status"]
|
|
646
646
|
json_response(git_status_payload)
|
|
647
|
+
# All four MCP surfaces (REST shim + JSON-RPC + SSE) go through
|
|
648
|
+
# with_mcp_gate: capability (Tina4.mcp_enabled?) decides whether MCP
|
|
649
|
+
# runs at all (off → route behaves as unmounted, nil → RackApp 404,
|
|
650
|
+
# no info leak); per-request authorisation on the RAW socket peer +
|
|
651
|
+
# token decides whether THIS caller may use it (loopback always,
|
|
652
|
+
# remote only with TINA4_MCP_REMOTE + a valid TINA4_MCP_TOKEN — denied
|
|
653
|
+
# → 404 mcp_forbidden). The dev tools expose powerful ops (DB query,
|
|
654
|
+
# file read/WRITE, route listing). Mirrors the Python master gate.
|
|
647
655
|
when ["GET", "/__dev/api/mcp/tools"]
|
|
648
|
-
json_response(mcp_tools_list)
|
|
656
|
+
with_mcp_gate(env) { json_response(mcp_tools_list) }
|
|
649
657
|
when ["POST", "/__dev/api/mcp/call"]
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
# route listing), so beyond the dev-toolbar's TINA4_DEBUG check they are
|
|
655
|
-
# gated on Tina4.mcp_enabled? — explicit TINA4_MCP wins on any host, else
|
|
656
|
-
# dev auto-enable is LOCALHOST-ONLY unless TINA4_MCP_REMOTE=true. Not
|
|
657
|
-
# enabled → falls through to the `else` (nil), so RackApp 404s it. They
|
|
658
|
-
# share the default MCP server's tool registry with the REST shim.
|
|
658
|
+
with_mcp_gate(env) do
|
|
659
|
+
body = read_json_body(env) || {}
|
|
660
|
+
json_response(mcp_tool_call(body))
|
|
661
|
+
end
|
|
659
662
|
when ["POST", "/__dev/mcp"], ["POST", "/__dev/mcp/message"]
|
|
660
|
-
|
|
663
|
+
with_mcp_gate(env) { mcp_jsonrpc(env) }
|
|
661
664
|
when ["GET", "/__dev/mcp/sse"]
|
|
662
|
-
|
|
665
|
+
with_mcp_gate(env) { mcp_sse_handshake }
|
|
663
666
|
when ["GET", "/__dev/api/scaffold"]
|
|
664
667
|
json_response(scaffold_templates)
|
|
665
668
|
when ["POST", "/__dev/api/scaffold/run"]
|
|
@@ -1586,6 +1589,70 @@ module Tina4
|
|
|
1586
1589
|
end
|
|
1587
1590
|
end
|
|
1588
1591
|
|
|
1592
|
+
# Gate an MCP surface. Yields to the handler block only for an authorised
|
|
1593
|
+
# caller. Returns:
|
|
1594
|
+
# nil — capability off (route behaves as unmounted → 404, no
|
|
1595
|
+
# "MCP exists" info leak; matches PHP not mounting it)
|
|
1596
|
+
# mcp_forbidden — capability on but this caller is denied (explicit 404)
|
|
1597
|
+
# block result — authorised caller proceeds
|
|
1598
|
+
def with_mcp_gate(env)
|
|
1599
|
+
return nil unless Tina4.mcp_enabled?
|
|
1600
|
+
return mcp_forbidden unless mcp_request_allowed?(env)
|
|
1601
|
+
|
|
1602
|
+
yield
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
# 404 payload for a disallowed MCP request (parity with Python/PHP).
|
|
1606
|
+
def mcp_forbidden
|
|
1607
|
+
[404, { "content-type" => "application/json; charset=utf-8" },
|
|
1608
|
+
[JSON.generate({ "error" => "MCP forbidden" })]]
|
|
1609
|
+
end
|
|
1610
|
+
|
|
1611
|
+
# Per-request MCP authorisation using the RAW socket peer.
|
|
1612
|
+
#
|
|
1613
|
+
# Reads env["REMOTE_ADDR"] (never X-Forwarded-For — spoofable) and the
|
|
1614
|
+
# token, then delegates to Tina4.request_allowed?. Loopback is always
|
|
1615
|
+
# allowed; a remote caller needs TINA4_MCP_REMOTE=true plus a valid
|
|
1616
|
+
# token. Mirrors the Python dev_admin _mcp_request_allowed.
|
|
1617
|
+
def mcp_request_allowed?(env)
|
|
1618
|
+
remote_ip = (env["REMOTE_ADDR"] || "").to_s
|
|
1619
|
+
Tina4.request_allowed?(remote_ip, has_valid_token: mcp_token_ok?(env))
|
|
1620
|
+
end
|
|
1621
|
+
|
|
1622
|
+
# Whether the request carried a token matching TINA4_MCP_TOKEN.
|
|
1623
|
+
#
|
|
1624
|
+
# Token transports (in order): Authorization: Bearer, X-MCP-Token,
|
|
1625
|
+
# X-Api-Key. Compared timing-safe against TINA4_MCP_TOKEN (fallback
|
|
1626
|
+
# TINA4_API_KEY). With NO configured token this returns false, so a
|
|
1627
|
+
# remote caller can never present a "valid" token by accident.
|
|
1628
|
+
def mcp_token_ok?(env)
|
|
1629
|
+
expected = ENV["TINA4_MCP_TOKEN"]
|
|
1630
|
+
expected = ENV["TINA4_API_KEY"] if expected.nil? || expected.empty?
|
|
1631
|
+
return false if expected.nil? || expected.empty?
|
|
1632
|
+
|
|
1633
|
+
provided = ""
|
|
1634
|
+
auth = (env["HTTP_AUTHORIZATION"] || "").to_s
|
|
1635
|
+
provided = auth[7..].to_s.strip if auth.downcase.start_with?("bearer ")
|
|
1636
|
+
provided = (env["HTTP_X_MCP_TOKEN"] || "").to_s if provided.empty?
|
|
1637
|
+
provided = (env["HTTP_X_API_KEY"] || "").to_s if provided.empty?
|
|
1638
|
+
return false if provided.empty?
|
|
1639
|
+
|
|
1640
|
+
secure_equal?(expected.to_s, provided)
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
# Constant-time string comparison (length check first so the
|
|
1644
|
+
# fixed-length compare never raises on a mismatch).
|
|
1645
|
+
def secure_equal?(expected, provided)
|
|
1646
|
+
return false unless expected.bytesize == provided.bytesize
|
|
1647
|
+
|
|
1648
|
+
OpenSSL.fixed_length_secure_compare(expected, provided)
|
|
1649
|
+
rescue StandardError
|
|
1650
|
+
# Fallback for very old OpenSSL without fixed_length_secure_compare.
|
|
1651
|
+
res = 0
|
|
1652
|
+
expected.bytes.zip(provided.bytes) { |a, b| res |= (a ^ b.to_i) }
|
|
1653
|
+
res.zero?
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1589
1656
|
def mcp_tools_list
|
|
1590
1657
|
return { tools: [], count: 0 } unless defined?(Tina4::McpServer)
|
|
1591
1658
|
server = Tina4._default_mcp_server
|