pinecall-runtime 0.1.6__py3-none-any.whl

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.
Files changed (328) hide show
  1. pinecall/__init__.py +0 -0
  2. pinecall/channels/__init__.py +0 -0
  3. pinecall/channels/_chooser.py +45 -0
  4. pinecall/channels/offers.py +219 -0
  5. pinecall/channels/rooms.py +176 -0
  6. pinecall/channels/routes.py +275 -0
  7. pinecall/channels/telephony/__init__.py +0 -0
  8. pinecall/channels/telephony/_twilio.py +349 -0
  9. pinecall/channels/telephony/carrier.py +229 -0
  10. pinecall/channels/telephony/carrier_catalog.py +92 -0
  11. pinecall/channels/telephony/carriers.csv +41 -0
  12. pinecall/channels/telephony/dialing.py +419 -0
  13. pinecall/channels/telephony/firewall.py +58 -0
  14. pinecall/channels/telephony/hand_over.py +78 -0
  15. pinecall/channels/telephony/number_path.py +176 -0
  16. pinecall/channels/telephony/numbers.py +530 -0
  17. pinecall/channels/telephony/origination.py +77 -0
  18. pinecall/channels/telephony/sip.py +448 -0
  19. pinecall/channels/whatsapp.py +413 -0
  20. pinecall/cli/__init__.py +0 -0
  21. pinecall/cli/_archive.py +85 -0
  22. pinecall/cli/_drift.py +38 -0
  23. pinecall/cli/_facts.py +67 -0
  24. pinecall/cli/_load.py +433 -0
  25. pinecall/cli/_operator.py +626 -0
  26. pinecall/cli/_sessions.py +164 -0
  27. pinecall/cli/_traceback.py +95 -0
  28. pinecall/cli/_usage.py +34 -0
  29. pinecall/cli/main.py +521 -0
  30. pinecall/domain/__init__.py +0 -0
  31. pinecall/domain/agent.py +446 -0
  32. pinecall/domain/call.py +152 -0
  33. pinecall/domain/country_codes.py +32 -0
  34. pinecall/domain/errors.py +151 -0
  35. pinecall/domain/names.py +115 -0
  36. pinecall/domain/org.py +120 -0
  37. pinecall/domain/person.py +208 -0
  38. pinecall/domain/scope.py +21 -0
  39. pinecall/evals/__init__.py +0 -0
  40. pinecall/evals/_evidence.py +158 -0
  41. pinecall/evals/calibration.py +112 -0
  42. pinecall/evals/callers.py +160 -0
  43. pinecall/evals/case.py +327 -0
  44. pinecall/evals/checks.py +437 -0
  45. pinecall/evals/compliance.py +197 -0
  46. pinecall/evals/dataset.py +451 -0
  47. pinecall/evals/goldens.py +331 -0
  48. pinecall/evals/judges.py +525 -0
  49. pinecall/evals/runs.py +252 -0
  50. pinecall/evals/spoken.py +356 -0
  51. pinecall/fleet/__init__.py +0 -0
  52. pinecall/fleet/client.py +569 -0
  53. pinecall/fleet/demand.py +51 -0
  54. pinecall/fleet/heartbeat.py +194 -0
  55. pinecall/fleet/measures.py +157 -0
  56. pinecall/fleet/roster.py +232 -0
  57. pinecall/fleet/worlds.py +44 -0
  58. pinecall/gateway/__init__.py +0 -0
  59. pinecall/gateway/_call_setup.py +53 -0
  60. pinecall/gateway/_deps.py +556 -0
  61. pinecall/gateway/_gateway.py +75 -0
  62. pinecall/gateway/_served.py +396 -0
  63. pinecall/gateway/_sockets.py +444 -0
  64. pinecall/gateway/_streams.py +83 -0
  65. pinecall/gateway/_text_calls.py +182 -0
  66. pinecall/gateway/api/__init__.py +0 -0
  67. pinecall/gateway/api/accounts.py +324 -0
  68. pinecall/gateway/api/agents.py +150 -0
  69. pinecall/gateway/api/apps.py +478 -0
  70. pinecall/gateway/api/box.py +658 -0
  71. pinecall/gateway/api/calibration.py +58 -0
  72. pinecall/gateway/api/callbacks.py +105 -0
  73. pinecall/gateway/api/calls.py +655 -0
  74. pinecall/gateway/api/chat.py +223 -0
  75. pinecall/gateway/api/dataset.py +163 -0
  76. pinecall/gateway/api/desk.py +113 -0
  77. pinecall/gateway/api/entries.py +77 -0
  78. pinecall/gateway/api/evals.py +647 -0
  79. pinecall/gateway/api/fleet.py +73 -0
  80. pinecall/gateway/api/hosting.py +241 -0
  81. pinecall/gateway/api/judges.py +97 -0
  82. pinecall/gateway/api/keys.py +110 -0
  83. pinecall/gateway/api/line.py +142 -0
  84. pinecall/gateway/api/members.py +197 -0
  85. pinecall/gateway/api/metrics.py +226 -0
  86. pinecall/gateway/api/numbers.py +476 -0
  87. pinecall/gateway/api/ops.py +428 -0
  88. pinecall/gateway/api/org.py +283 -0
  89. pinecall/gateway/api/personas.py +157 -0
  90. pinecall/gateway/api/pipeline.py +206 -0
  91. pinecall/gateway/api/prompts.py +50 -0
  92. pinecall/gateway/api/providers.py +240 -0
  93. pinecall/gateway/api/recordings.py +89 -0
  94. pinecall/gateway/api/relay.py +169 -0
  95. pinecall/gateway/api/retrieval.py +403 -0
  96. pinecall/gateway/api/runner.py +109 -0
  97. pinecall/gateway/api/sessions.py +102 -0
  98. pinecall/gateway/api/settings.py +463 -0
  99. pinecall/gateway/api/signup.py +164 -0
  100. pinecall/gateway/api/sso_login.py +168 -0
  101. pinecall/gateway/api/threads.py +187 -0
  102. pinecall/gateway/api/usage.py +303 -0
  103. pinecall/gateway/api/visitors.py +222 -0
  104. pinecall/gateway/api/whatsapp.py +48 -0
  105. pinecall/gateway/api/widget.py +49 -0
  106. pinecall/gateway/app.py +504 -0
  107. pinecall/gateway/calls/__init__.py +0 -0
  108. pinecall/gateway/calls/binding.py +63 -0
  109. pinecall/gateway/calls/commands.py +18 -0
  110. pinecall/gateway/calls/inbox.py +100 -0
  111. pinecall/gateway/calls/known.py +23 -0
  112. pinecall/gateway/calls/owners.py +87 -0
  113. pinecall/gateway/calls/pump.py +33 -0
  114. pinecall/gateway/calls/serving.py +118 -0
  115. pinecall/gateway/calls/threads.py +367 -0
  116. pinecall/gateway/dispatching/__init__.py +0 -0
  117. pinecall/gateway/dispatching/arrivals.py +58 -0
  118. pinecall/gateway/dispatching/sweep.py +43 -0
  119. pinecall/gateway/ending/__init__.py +0 -0
  120. pinecall/gateway/ending/reaper.py +160 -0
  121. pinecall/gateway/ending/seal.py +312 -0
  122. pinecall/gateway/ending/stranded.py +117 -0
  123. pinecall/log/__init__.py +0 -0
  124. pinecall/log/_histogram.py +74 -0
  125. pinecall/log/_relay.py +259 -0
  126. pinecall/log/_writer.py +454 -0
  127. pinecall/log/days.py +154 -0
  128. pinecall/log/drift.py +550 -0
  129. pinecall/log/facts.py +358 -0
  130. pinecall/log/inbox.py +169 -0
  131. pinecall/log/lists.py +155 -0
  132. pinecall/log/logs.py +581 -0
  133. pinecall/log/openings.py +71 -0
  134. pinecall/log/private.py +135 -0
  135. pinecall/log/queries.py +229 -0
  136. pinecall/log/readers.py +207 -0
  137. pinecall/log/reduce.py +659 -0
  138. pinecall/log/refold.py +217 -0
  139. pinecall/log/store.py +370 -0
  140. pinecall/log/usage.py +242 -0
  141. pinecall/postgres/__init__.py +0 -0
  142. pinecall/postgres/migrate.py +202 -0
  143. pinecall/postgres/migrations/0001_schema.sql +591 -0
  144. pinecall/postgres/migrations/0002_worlds.sql +15 -0
  145. pinecall/postgres/migrations/0003_lent.sql +4 -0
  146. pinecall/postgres/migrations/0004_carriers.sql +23 -0
  147. pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
  148. pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
  149. pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
  150. pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
  151. pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
  152. pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
  153. pinecall/postgres/migrations/0011_erasure.sql +32 -0
  154. pinecall/postgres/migrations/0012_org_policy.sql +10 -0
  155. pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
  156. pinecall/postgres/migrations/0014_consents.sql +24 -0
  157. pinecall/postgres/migrations/0015_disclosure.sql +7 -0
  158. pinecall/postgres/migrations/0016_call_records.sql +21 -0
  159. pinecall/postgres/migrations/0017_reads.sql +17 -0
  160. pinecall/postgres/migrations/0018_hosting.sql +41 -0
  161. pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
  162. pinecall/postgres/migrations/0020_written_once.sql +7 -0
  163. pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
  164. pinecall/postgres/migrations/0022_sealed.sql +39 -0
  165. pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
  166. pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
  167. pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
  168. pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
  169. pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
  170. pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
  171. pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
  172. pinecall/postgres/migrations/0060_drift.sql +62 -0
  173. pinecall/postgres/migrations/0061_canaries.sql +24 -0
  174. pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
  175. pinecall/postgres/migrations/0063_prompts.sql +16 -0
  176. pinecall/postgres/migrations/0075_call_private.sql +14 -0
  177. pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
  178. pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
  179. pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
  180. pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
  181. pinecall/postgres/migrations/0080_call_openings.sql +20 -0
  182. pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
  183. pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
  184. pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
  185. pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
  186. pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
  187. pinecall/postgres/migrations/0086_route_origin.sql +17 -0
  188. pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
  189. pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
  190. pinecall/postgres/migrations/0089_offers.sql +21 -0
  191. pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
  192. pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
  193. pinecall/postgres/migrations/0092_key_parent.sql +6 -0
  194. pinecall/postgres/migrations/0093_code_tries.sql +10 -0
  195. pinecall/postgres/migrations/0094_route_approval.sql +12 -0
  196. pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
  197. pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
  198. pinecall/postgres/migrations/0097_case_status.sql +19 -0
  199. pinecall/postgres/pool.py +190 -0
  200. pinecall/process/__init__.py +0 -0
  201. pinecall/process/_objects.py +97 -0
  202. pinecall/process/box_settings.py +23 -0
  203. pinecall/process/connections.py +124 -0
  204. pinecall/process/metrics.py +127 -0
  205. pinecall/process/recordings.py +323 -0
  206. pinecall/process/resolver.py +41 -0
  207. pinecall/process/sealed_audio.py +173 -0
  208. pinecall/process/settings.py +534 -0
  209. pinecall/process/shared.py +158 -0
  210. pinecall/process/signal.py +416 -0
  211. pinecall/providers/__init__.py +0 -0
  212. pinecall/providers/build.py +570 -0
  213. pinecall/providers/catalog.py +266 -0
  214. pinecall/providers/credentials.py +230 -0
  215. pinecall/providers/declared.py +69 -0
  216. pinecall/providers/prices.py +240 -0
  217. pinecall/providers/tried.py +104 -0
  218. pinecall/providers/voices.py +174 -0
  219. pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
  220. pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
  221. pinecall/public/console/favicon.png +0 -0
  222. pinecall/public/console/index.html +20 -0
  223. pinecall/public/console/logo-mark.png +0 -0
  224. pinecall/public/console/pinecall-logo-dark.png +0 -0
  225. pinecall/public/console/pinecall-logo.png +0 -0
  226. pinecall/public/console/pinecall-mark.png +0 -0
  227. pinecall/public/console/sw.js +37 -0
  228. pinecall/public/widget/pinecall-widget.js +853 -0
  229. pinecall/py.typed +0 -0
  230. pinecall/retrieval/__init__.py +0 -0
  231. pinecall/retrieval/_search.py +186 -0
  232. pinecall/retrieval/_sections.py +70 -0
  233. pinecall/retrieval/embed.py +340 -0
  234. pinecall/retrieval/extraction.py +403 -0
  235. pinecall/retrieval/knowledge.py +659 -0
  236. pinecall/retrieval/lookups.py +192 -0
  237. pinecall/retrieval/memory.py +603 -0
  238. pinecall/runner/__init__.py +0 -0
  239. pinecall/runner/_kube.py +459 -0
  240. pinecall/runner/_plan.py +157 -0
  241. pinecall/runner/main.py +306 -0
  242. pinecall/session/__init__.py +0 -0
  243. pinecall/session/_agent.py +141 -0
  244. pinecall/session/_hearing.py +101 -0
  245. pinecall/session/_livekit.py +195 -0
  246. pinecall/session/_prompt.py +147 -0
  247. pinecall/session/_recording.py +237 -0
  248. pinecall/session/call.py +367 -0
  249. pinecall/session/clock.py +40 -0
  250. pinecall/session/hold.py +239 -0
  251. pinecall/session/room.py +626 -0
  252. pinecall/session/session.py +690 -0
  253. pinecall/session/text.py +144 -0
  254. pinecall/session/tools.py +483 -0
  255. pinecall/session/voice.py +118 -0
  256. pinecall/session/widget.py +201 -0
  257. pinecall/tenancy/__init__.py +0 -0
  258. pinecall/tenancy/admission.py +204 -0
  259. pinecall/tenancy/agents.py +164 -0
  260. pinecall/tenancy/canary.py +126 -0
  261. pinecall/tenancy/carrier_networks.py +156 -0
  262. pinecall/tenancy/carriers.py +265 -0
  263. pinecall/tenancy/codes.py +196 -0
  264. pinecall/tenancy/consents.py +167 -0
  265. pinecall/tenancy/dial_policy.py +405 -0
  266. pinecall/tenancy/disclosure.py +41 -0
  267. pinecall/tenancy/erasure.py +204 -0
  268. pinecall/tenancy/export.py +167 -0
  269. pinecall/tenancy/hosted_running.py +166 -0
  270. pinecall/tenancy/hosting.py +475 -0
  271. pinecall/tenancy/judges.py +126 -0
  272. pinecall/tenancy/keys.py +467 -0
  273. pinecall/tenancy/knocks.py +40 -0
  274. pinecall/tenancy/letters.py +378 -0
  275. pinecall/tenancy/mail.py +458 -0
  276. pinecall/tenancy/org_secrets.py +123 -0
  277. pinecall/tenancy/orgs.py +78 -0
  278. pinecall/tenancy/people.py +543 -0
  279. pinecall/tenancy/personas.py +145 -0
  280. pinecall/tenancy/policy.py +65 -0
  281. pinecall/tenancy/prompts.py +48 -0
  282. pinecall/tenancy/reads.py +73 -0
  283. pinecall/tenancy/recording_keys.py +41 -0
  284. pinecall/tenancy/remembered.py +148 -0
  285. pinecall/tenancy/retention.py +86 -0
  286. pinecall/tenancy/scopes.py +406 -0
  287. pinecall/tenancy/signin.py +622 -0
  288. pinecall/tenancy/spend.py +69 -0
  289. pinecall/tenancy/sso.py +431 -0
  290. pinecall/tenancy/sso_domains.py +132 -0
  291. pinecall/tenancy/throttle.py +88 -0
  292. pinecall/tenancy/tokens.py +400 -0
  293. pinecall/tenancy/traceback.py +70 -0
  294. pinecall/tenancy/usage.py +136 -0
  295. pinecall/tenancy/vault.py +204 -0
  296. pinecall/tenancy/words.py +144 -0
  297. pinecall/wire/__init__.py +0 -0
  298. pinecall/wire/commands.py +350 -0
  299. pinecall/wire/events.py +655 -0
  300. pinecall/wire/frames.py +65 -0
  301. pinecall/wire/metrics.py +287 -0
  302. pinecall/wire/parts.py +320 -0
  303. pinecall/wire/rest/__init__.py +0 -0
  304. pinecall/wire/rest/accounts.py +512 -0
  305. pinecall/wire/rest/agents.py +172 -0
  306. pinecall/wire/rest/calls.py +397 -0
  307. pinecall/wire/rest/evals.py +453 -0
  308. pinecall/wire/rest/fleet.py +69 -0
  309. pinecall/wire/rest/hosting.py +174 -0
  310. pinecall/wire/rest/numbers.py +280 -0
  311. pinecall/wire/rest/ops.py +342 -0
  312. pinecall/wire/rest/providers.py +83 -0
  313. pinecall/wire/rest/retrieval.py +301 -0
  314. pinecall/wire/rest/settings.py +184 -0
  315. pinecall/wire/rest/usage.py +245 -0
  316. pinecall/wire/room.py +61 -0
  317. pinecall/wire/scores.py +49 -0
  318. pinecall/wire/state.py +243 -0
  319. pinecall/worker/__init__.py +0 -0
  320. pinecall/worker/_job.py +655 -0
  321. pinecall/worker/_recorder.py +82 -0
  322. pinecall/worker/_traces.py +48 -0
  323. pinecall/worker/main.py +307 -0
  324. pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
  325. pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
  326. pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
  327. pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
  328. pinecall_runtime-0.1.6.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,556 @@
1
+ """What a request is: the gateway it reaches, the key, its world and scope, a reader."""
2
+
3
+ import asyncio
4
+ import math
5
+ import time
6
+ from collections.abc import Awaitable, Callable, Mapping
7
+ from dataclasses import dataclass
8
+ from hmac import compare_digest
9
+ from typing import Annotated
10
+
11
+ from fastapi import Depends, HTTPException, Query
12
+ from starlette.requests import HTTPConnection, Request
13
+
14
+ from pinecall.domain.agent import AgentConfig
15
+ from pinecall.domain.errors import (
16
+ NotAllowed,
17
+ NotAvailable,
18
+ NotFound,
19
+ NotSignedIn,
20
+ QuotaExhausted,
21
+ Throttled,
22
+ TooLarge,
23
+ TooManyRequests,
24
+ )
25
+ from pinecall.domain.names import Env, parse_env
26
+ from pinecall.domain.person import HOLDING, THE_FLEET, THE_RUNNER, THE_TEAM, KeyScope
27
+ from pinecall.domain.scope import Scope
28
+ from pinecall.gateway._call_setup import exhausted
29
+ from pinecall.gateway._gateway import Gateway
30
+ from pinecall.log import queries
31
+ from pinecall.postgres.pool import scope_to
32
+ from pinecall.retrieval.embed import Embedder
33
+ from pinecall.tenancy import admission, keys, people, reads, throttle, tokens
34
+ from pinecall.tenancy.keys import Bearer
35
+ from pinecall.tenancy.knocks import Throttle
36
+ from pinecall.tenancy.reads import Read
37
+ from pinecall.tenancy.tokens import PROJECTION_OF, Visit
38
+ from pinecall.wire.frames import Entry, WireModel
39
+ from pinecall.wire.parts import Projection
40
+ from pinecall.wire.rest.agents import ScopeHolder
41
+ from pinecall.wire.rest.calls import ReadKind
42
+
43
+ # The world a person's key acts in; a server's key is its own world whatever this says.
44
+ WORLD = "pinecall-env"
45
+
46
+ # An admin, in the sandbox, looking into a colleague's scope.
47
+ LOOKING_AT = "pinecall-corner"
48
+
49
+
50
+ TAKES_A_KEY = "this door takes an API key"
51
+
52
+
53
+ READ_WITH_A_KEY = "a log is read with a key, or with a token of that call"
54
+
55
+
56
+ NOT_THE_OPERATORS = "this door is the box's: its operator key, or a person the box made an operator"
57
+
58
+
59
+ # Who decided, when the box's own key did: it names no person.
60
+ OPS_KEY_NAMED = "the box's key"
61
+
62
+
63
+ NOT_STARTED = "the gateway never started: its lifespan never ran"
64
+ NO_EMBEDDER = (
65
+ "this box embeds nothing: its providers row names no embedding, or the box holds no key for it"
66
+ )
67
+
68
+
69
+ SCOPES_OF: dict[Callable[..., object], frozenset[KeyScope]] = {}
70
+
71
+
72
+ # RFC 6455 §5.5: a longer close reason drops the connection with no close frame.
73
+ CLOSE_REASON_BYTES = 123
74
+
75
+
76
+ POLICY_VIOLATION = 1008
77
+
78
+
79
+ # A listing's page is never longer than this, whatever the reader asks.
80
+ LONGEST_LIST = 200
81
+
82
+
83
+ NO_SUCH_CALL = "no call {call} in this key's org and world"
84
+
85
+
86
+ NOT_YOURS = "this token reads another call"
87
+
88
+
89
+ NEVER_OPENED = "no call {call} was opened: the fleet's key reads and acts for a call once it is"
90
+
91
+
92
+ # How a family of doors is named: the scopes it opens, as the page of doors writes them.
93
+ FAMILY = " · "
94
+
95
+
96
+ # The platform's own keys are never paced: its workers and its runner.
97
+ UNPACED: frozenset[KeyScope] = frozenset({THE_FLEET, THE_RUNNER})
98
+
99
+
100
+ PACED = (
101
+ "this org sent its {family} doors {limit} requests this minute, in {env}: "
102
+ "try again in {seconds} s"
103
+ )
104
+
105
+
106
+ # A webhook carries no key, so its body is read before anything says who sent it: no more than
107
+ # this, above Meta's own ceiling (3 MB a delivery), far above a LiveKit event's few kilobytes.
108
+ LARGEST_WEBHOOK = 4 * 1024 * 1024
109
+
110
+
111
+ WEBHOOK_TOO_LARGE = "a webhook's body is over 4 MiB, more than any sender of one writes"
112
+
113
+
114
+ NOBODY_ISSUED = (
115
+ "no code {code} is waiting for agent {agent}: nobody issued it, it expired, or a call took it"
116
+ )
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class Acting:
121
+ """A key as it acts in this request: the bearer and the world."""
122
+
123
+ bearer: Bearer
124
+ env: Env
125
+
126
+ @property
127
+ def org(self) -> str:
128
+ """The key's org."""
129
+ return self.bearer.key.org
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class Reader:
134
+ """Who reads a log: a key in its scope, or a token of one call."""
135
+
136
+ acting: Acting | None = None
137
+ scope: Scope | None = None
138
+ visit: Visit | None = None
139
+
140
+ @property
141
+ def projection(self) -> Projection:
142
+ """What the reader sees of an entry: a key sees the tenant's, a token its own grant's."""
143
+ if self.visit is None:
144
+ return "tenant"
145
+ return self.visit.projection or PROJECTION_OF[self.visit.scope]
146
+
147
+
148
+ # The header only, never the URL: a URL lands in logs.
149
+ def bearer_of(headers: Mapping[str, str]) -> str | None:
150
+ """The bearer the Authorization header carries."""
151
+ answer = headers.get("authorization", "")
152
+ scheme, _, value = answer.partition(" ")
153
+ return (value.strip() or None) if scheme.lower() == "bearer" else None
154
+
155
+
156
+ # Only the fleet's key names a scope: the scope of the call it serves.
157
+ def dispatched(
158
+ org: Annotated[str | None, Query()] = None,
159
+ env: Annotated[str | None, Query()] = None,
160
+ holder: Annotated[str | None, Query()] = None,
161
+ ) -> Scope | None:
162
+ """The scope a worker asks in, from the dispatch it was started with."""
163
+ if org is None or env is None:
164
+ return None
165
+ return Scope(org, parse_env(env), holder or "")
166
+
167
+
168
+ DispatchedDep = Annotated[Scope | None, Depends(dispatched)]
169
+
170
+
171
+ def closing(connection: HTTPConnection) -> asyncio.Event:
172
+ """The event set when the process is told to stop."""
173
+ return gateway_of(connection).closing
174
+
175
+
176
+ def close_reason(sentence: str) -> str:
177
+ """The sentence cut to what a close frame carries."""
178
+ return sentence.encode("utf-8")[:CLOSE_REASON_BYTES].decode("utf-8", errors="ignore")
179
+
180
+
181
+ # uvicorn reads X-Forwarded-For only from the box's own Caddy (forwarded_allow_ips), so the
182
+ # client here is the person's address behind it and nobody else's word.
183
+ def client_of(request: Request) -> str:
184
+ """The address a request came from, as the throttle counts it."""
185
+ return request.client.host if request.client is not None else "unknown"
186
+
187
+
188
+ async def check_knock(gateway: Gateway, name: str, refusal: str) -> None:
189
+ """Count a knock of the name; TooManyRequests with the refusal past five in a minute."""
190
+ if not await gateway.signins.throttle.allowed(name):
191
+ raise TooManyRequests(refusal)
192
+
193
+
194
+ # A door that runs a model per request, on keys the box may lend: so many a minute per org, on
195
+ # every gateway, whatever its family's wall allows.
196
+ async def check_paced(gateway: Gateway, name: str, tries: int, refusal: str) -> None:
197
+ """Count one of the name's model runs; TooManyRequests past `tries` in a minute."""
198
+ pace = Throttle(gateway.connections.pool, tries, gateway.signins.throttle.clock)
199
+ if not await pace.allowed(name):
200
+ raise TooManyRequests(refusal)
201
+
202
+
203
+ # Never the request's own Host where the box has a name: a forged host would send a sign-in or
204
+ # a password link to whoever forged it.
205
+ def public_url(request: Request, gateway: Gateway) -> str:
206
+ """The box's address, else the one the request came in by, without a trailing slash."""
207
+ return gateway.connections.settings.address or str(request.base_url).rstrip("/")
208
+
209
+
210
+ async def webhook_body(request: Request) -> bytes:
211
+ """The unsigned body of a webhook, refused past LARGEST_WEBHOOK before it is read further."""
212
+ body = await capped_body(request, LARGEST_WEBHOOK)
213
+ if len(body) > LARGEST_WEBHOOK:
214
+ raise TooLarge(WEBHOOK_TOO_LARGE)
215
+ return body
216
+
217
+
218
+ # Read a chunk at a time and stopped just past the most: an upload over its size is refused
219
+ # without having landed whole in memory first.
220
+ async def capped_body(request: Request, most: int) -> bytes:
221
+ """The request's body, or its first `most` bytes and a chunk more when it is longer."""
222
+ read = bytearray()
223
+ async for chunk in request.stream():
224
+ read += chunk
225
+ if len(read) > most:
226
+ break
227
+ return bytes(read)
228
+
229
+
230
+ async def admit_call(gateway: Gateway, scope: Scope, agent: str) -> admission.Ceiling | None:
231
+ """Admit one more call of the org in its world, or write the refusal to the agent's log."""
232
+ try:
233
+ return await admission.admit_call(
234
+ gateway.connections.pool,
235
+ scope.org,
236
+ scope.env,
237
+ running=gateway.live.running(scope.org, scope.env),
238
+ at=gateway.logs.store.clock(),
239
+ )
240
+ except QuotaExhausted as refused:
241
+ await exhausted(gateway.logs, scope.org, agent, refused)
242
+ raise
243
+
244
+
245
+ # A box with no embedder still starts: the doors that embed refuse, and every other answers.
246
+ def embedder_of(gateway: Gateway) -> Embedder:
247
+ """The box's embedder; NotAvailable in one sentence when it has none."""
248
+ if gateway.embedder is None:
249
+ raise NotAvailable(NO_EMBEDDER)
250
+ return gateway.embedder
251
+
252
+
253
+ def gateway_of(connection: HTTPConnection) -> Gateway:
254
+ """The gateway as its lifespan built it."""
255
+ found: object = getattr(connection.app.state, "gateway", None)
256
+ if not isinstance(found, Gateway):
257
+ raise HTTPException(503, NOT_STARTED)
258
+ return found
259
+
260
+
261
+ GatewayDep = Annotated[Gateway, Depends(gateway_of)]
262
+
263
+
264
+ async def bearer(connection: HTTPConnection, gateway: GatewayDep) -> Bearer:
265
+ """The key that knocked, and its person; 401 without saying why."""
266
+ data = bearer_of(connection.headers)
267
+ verified = None if data is None or tokens.is_a_jwt(data) else await gateway.keys.verify(data)
268
+ if verified is None:
269
+ raise NotSignedIn(TAKES_A_KEY)
270
+ _scoped_to_its_org(verified)
271
+ return verified
272
+
273
+
274
+ BearerDep = Annotated[Bearer, Depends(bearer)]
275
+
276
+
277
+ async def operator(connection: HTTPConnection, gateway: GatewayDep) -> str:
278
+ """The box's own key, or a person the box made an operator, by name; 401 otherwise."""
279
+ data = bearer_of(connection.headers)
280
+ ops = gateway.connections.settings.ops_key
281
+ if data is not None and ops and compare_digest(data, ops):
282
+ return OPS_KEY_NAMED
283
+ verified = None if data is None else await gateway.keys.verify(data)
284
+ if verified is None or verified.member is None or not verified.member.operator:
285
+ raise NotSignedIn(NOT_THE_OPERATORS)
286
+ return verified.member.email
287
+
288
+
289
+ OperatorDep = Annotated[str, Depends(operator)]
290
+
291
+
292
+ async def named_holder(gateway: Gateway, scope: Scope) -> ScopeHolder | None:
293
+ """Who holds the scope, by member id and email; None for the org's own."""
294
+ if not scope.holder:
295
+ return None
296
+ member = await people.find(gateway.connections.pool, scope.org, scope.holder)
297
+ return ScopeHolder(holder=scope.holder, name=None if member is None else member.email)
298
+
299
+
300
+ # A team reader with the holding scope sees every scope of the org; anyone else their own.
301
+ def sees_every_scope(key: Acting) -> bool:
302
+ """Whether the key reads every holder's agents in its org and world."""
303
+ return {THE_TEAM, HOLDING} <= key.bearer.key.scopes
304
+
305
+
306
+ async def acting(connection: HTTPConnection, key: BearerDep) -> Acting:
307
+ """The key as it acts here: a server's key in its world, a person's in the world asked."""
308
+ return Acting(bearer=key, env=world_of_request(connection, key))
309
+
310
+
311
+ def world_of_request(connection: HTTPConnection, key: Bearer) -> Env:
312
+ """The world a request is for: the key's own, else the one its header asks for."""
313
+ return keys.world_of(key, connection.headers.get(WORLD))
314
+
315
+
316
+ ActingDep = Annotated[Acting, Depends(acting)]
317
+
318
+
319
+ # The one record of who asked, which is what audits a dial and an erasure.
320
+ def asked_by(key: Acting) -> str:
321
+ """The person behind the key, or the key itself for a server's."""
322
+ member = key.bearer.member
323
+ return member.id if member is not None else key.bearer.key.key_id
324
+
325
+
326
+ # A person's read and a server's are written down, by the person or the key; a visitor reads
327
+ # its own call and the fleet the call it serves, neither of which is an access to record.
328
+ async def record_read(gateway: Gateway, reading: Reader, call: str, what: ReadKind) -> None:
329
+ """Write down who read the call, and what of it."""
330
+ acting = reading.acting
331
+ if acting is None or reading.scope is None:
332
+ return
333
+ await reads.record(gateway.connections.pool, reading.scope, Read(call, what, asked_by(acting)))
334
+
335
+
336
+ def ephemeral_entry(slug: str, event: WireModel, *, kind: str = "error") -> Entry:
337
+ """An entry sent to an app socket and never stored: no call, no seq."""
338
+ return Entry(
339
+ seq=0,
340
+ ts=time.time(),
341
+ call=None,
342
+ agent=slug,
343
+ type=kind,
344
+ ephemeral=True,
345
+ data=event.written(),
346
+ )
347
+
348
+
349
+ # FastAPI calls these: one per scope, each recorded so a test walks every door for exactly one.
350
+ def opening(*scopes: KeyScope) -> Callable[[HTTPConnection, Acting, Gateway], Awaitable[Acting]]:
351
+ """A dependency that lets through a key that opens one of the scopes."""
352
+ family = FAMILY.join(sorted(scopes))
353
+
354
+ async def opened(connection: HTTPConnection, key: ActingDep, gateway: GatewayDep) -> Acting:
355
+ keys.check_opens(key.bearer, *scopes)
356
+ _check_agent_named(connection, key.bearer)
357
+ if THE_FLEET not in scopes:
358
+ await _paced(gateway, key, family)
359
+ return key
360
+
361
+ SCOPES_OF[opened] = frozenset(scopes)
362
+ return opened
363
+
364
+
365
+ # The declaration is read by a worker (the fleet's key, or a tenant's own on app) and by
366
+ # whoever reads the calls it made (calls).
367
+ DeclarationKey = Annotated[Acting, Depends(opening("app", "calls", "fleet"))]
368
+
369
+
370
+ # A worker knocks with its fleet's key; a tenant's own worker with an app key.
371
+ WorkerKey = Annotated[Acting, Depends(opening("app", "fleet"))]
372
+
373
+
374
+ FleetKey = Annotated[Acting, Depends(opening("fleet"))]
375
+
376
+
377
+ # The box's runner of a world: it is told every org's hosted apps, and handed what starts them.
378
+ RunnerKey = Annotated[Acting, Depends(opening("runner"))]
379
+
380
+
381
+ NumbersKey = Annotated[Acting, Depends(opening("numbers"))]
382
+
383
+
384
+ UsageKey = Annotated[Acting, Depends(opening("usage"))]
385
+
386
+
387
+ EvalsKey = Annotated[Acting, Depends(opening("evals"))]
388
+
389
+ KnowledgeKey = Annotated[Acting, Depends(opening("knowledge"))]
390
+
391
+ MemoryKey = Annotated[Acting, Depends(opening("memory"))]
392
+
393
+
394
+ WordsKey = Annotated[Acting, Depends(opening("pipeline", "words"))]
395
+
396
+
397
+ PipelineKey = Annotated[Acting, Depends(opening("pipeline"))]
398
+
399
+
400
+ ProvidersKey = Annotated[Acting, Depends(opening("providers"))]
401
+
402
+
403
+ SuperviseKey = Annotated[Acting, Depends(opening("supervise"))]
404
+
405
+
406
+ TalkKey = Annotated[Acting, Depends(opening("talk"))]
407
+
408
+
409
+ CallsKey = Annotated[Acting, Depends(opening("calls"))]
410
+
411
+
412
+ AppKey = Annotated[Acting, Depends(opening("app"))]
413
+
414
+
415
+ TeamKey = Annotated[Acting, Depends(opening("team"))]
416
+
417
+
418
+ # Any other key acts in its own scope, or, an admin's in the sandbox, in the colleague named.
419
+ async def scope(
420
+ connection: HTTPConnection, key: ActingDep, gateway: GatewayDep, named: DispatchedDep
421
+ ) -> Scope:
422
+ """The scope this request acts in."""
423
+ survey = connection.headers.get(LOOKING_AT)
424
+ looking_at = None
425
+ if survey and key.bearer.member is not None:
426
+ looking_at = await people.find(gateway.connections.pool, key.org, survey)
427
+ if looking_at is None:
428
+ raise NotAllowed(keys.NOT_A_COLLEAGUE)
429
+ called = await _called(gateway, connection) if THE_FLEET in key.bearer.key.scopes else None
430
+ return keys.scope_of(
431
+ key.bearer, key.env, looking_at=looking_at, dispatched=named, called=called
432
+ )
433
+
434
+
435
+ ScopeDep = Annotated[Scope, Depends(scope)]
436
+
437
+
438
+ # ?token= only for a token of ours: EventSource sets no header, and a key in a URL would leak.
439
+ # A worker reads the call it serves with the fleet's key and names no scope to do it, since the
440
+ # call's head row says whose it is: only a call's own reading doors (its events, its state, its
441
+ # recording) let that key in.
442
+ def reading(*opens: KeyScope) -> Callable[..., Awaitable[Reader]]:
443
+ """A dependency that lets through a key that opens one of the scopes, or a call's token."""
444
+
445
+ async def read(
446
+ connection: HTTPConnection,
447
+ gateway: GatewayDep,
448
+ named: DispatchedDep,
449
+ token: Annotated[str | None, Query()] = None,
450
+ ) -> Reader:
451
+ data = bearer_of(connection.headers) or token
452
+ if data is None:
453
+ raise NotSignedIn(READ_WITH_A_KEY)
454
+ if tokens.is_a_jwt(data):
455
+ visit = tokens.read(gateway.signer, data)
456
+ if visit is None:
457
+ raise NotSignedIn(READ_WITH_A_KEY)
458
+ return Reader(visit=visit)
459
+ verified = None if data == token else await gateway.keys.verify(data)
460
+ if verified is None:
461
+ raise NotSignedIn(READ_WITH_A_KEY)
462
+ key = Acting(bearer=verified, env=world_of_request(connection, verified))
463
+ keys.check_opens(verified, *opens)
464
+ _check_agent_named(connection, verified)
465
+ await _paced(gateway, key, "calls")
466
+ if THE_FLEET in verified.key.scopes:
467
+ return Reader(acting=key)
468
+ _scoped_to_its_org(verified)
469
+ return Reader(acting=key, scope=await scope(connection, key, gateway, named))
470
+
471
+ SCOPES_OF[read] = frozenset(opens)
472
+ return read
473
+
474
+
475
+ ReaderDep = Annotated[Reader, Depends(reading("calls"))]
476
+
477
+
478
+ CallReaderDep = Annotated[Reader, Depends(reading("calls", THE_FLEET))]
479
+
480
+
481
+ # A key reads a call of its org in its world; a token, its own call alone. A call nobody wrote
482
+ # yet is empty to a key, not another org's: a client that minted the id tails it before the room
483
+ # opens. The fleet's key reads only a call that was opened, of the world it serves.
484
+ async def check_readable(gateway: Gateway, reading: Reader, call: str) -> AgentConfig | None:
485
+ """Refuse a reader the call is not theirs; the declaration of its agent, when held."""
486
+ if reading.visit is not None and reading.visit.call != call:
487
+ raise NotAllowed(NOT_YOURS)
488
+ kept = await queries.scope_of_call(gateway.connections.pool, call)
489
+ fleet = reading.acting is not None and THE_FLEET in reading.acting.bearer.key.scopes
490
+ if fleet and (kept is None or kept.scope is None):
491
+ raise NotFound(NEVER_OPENED.format(call=call))
492
+ if kept is None:
493
+ return None
494
+ if not _sees(reading, kept.scope):
495
+ raise NotFound(NO_SUCH_CALL.format(call=call))
496
+ if reading.acting is not None:
497
+ keys.check_agent(reading.acting.bearer, kept.agent)
498
+ return gateway.sockets.declared(kept.agent)
499
+
500
+
501
+ # The org's own calls and the reader's own scope; a token is checked by its call, the fleet's
502
+ # key reads the calls of the world it serves and none of the other.
503
+ def _sees(reading: Reader, owner: Scope | None) -> bool:
504
+ if reading.visit is not None:
505
+ return True
506
+ if reading.acting is not None and THE_FLEET in reading.acting.bearer.key.scopes:
507
+ return owner is not None and owner.env == reading.acting.env
508
+ reader = reading.scope
509
+ if reader is None or owner is None:
510
+ return False
511
+ same = reader.org == owner.org and reader.env == owner.env
512
+ return same and owner.holder in ("", reader.holder)
513
+
514
+
515
+ # A worker's request that names a call, in its path or as ?call=, acts in that call's scope as its
516
+ # head row keeps it; a call nobody opened yet is refused rather than trusted.
517
+ async def _called(gateway: Gateway, connection: HTTPConnection) -> Scope | None:
518
+ call = connection.path_params.get("call") or connection.query_params.get("call")
519
+ if not call:
520
+ return None
521
+ kept = await queries.scope_of_call(gateway.connections.pool, call)
522
+ if kept is None or kept.scope is None:
523
+ raise NotFound(NEVER_OPENED.format(call=call))
524
+ return kept.scope
525
+
526
+
527
+ # The agent a door names in its path or its query; a body's agent is checked by its door.
528
+ def _check_agent_named(connection: HTTPConnection, bearer: Bearer) -> None:
529
+ for slug in (connection.path_params.get("slug"), connection.query_params.get("agent")):
530
+ if slug:
531
+ keys.check_agent(bearer, slug)
532
+
533
+
534
+ # One count per org, world and family: a noisy tenant waits, and nobody else does. A live call's
535
+ # own doors are never paced: admission already bounds them, and a worker reads a 4xx as final.
536
+ async def _paced(gateway: Gateway, key: Acting, family: str) -> None:
537
+ bearer = key.bearer
538
+ if bearer.key.scopes & UNPACED or (bearer.member is not None and bearer.member.operator):
539
+ return
540
+ name = f"{key.org}/{key.env}/{family}"
541
+ wait = await gateway.paced.counted(name, throttle.REQUESTS_A_MINUTE)
542
+ if wait is None:
543
+ return
544
+ seconds = max(1, math.ceil(wait))
545
+ raise Throttled(
546
+ PACED.format(family=family, limit=throttle.REQUESTS_A_MINUTE, env=key.env, seconds=seconds),
547
+ retry_after_s=seconds,
548
+ )
549
+
550
+
551
+ # A tenant's key acts in its org alone, and from the moment it is verified so does every connection
552
+ # of the request (postgres/pool.py, migration 0096). The fleet's and the runner's serve every org
553
+ # of a world. A door that reads another org of the same person says so with `box_wide()`.
554
+ def _scoped_to_its_org(key: Bearer) -> None:
555
+ if not key.key.scopes & {THE_FLEET, THE_RUNNER}:
556
+ scope_to(key.key.org)
@@ -0,0 +1,75 @@
1
+ """The gateway process: its connections and the state it keeps in memory while it serves."""
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass, field
5
+
6
+ from pinecall.channels.offers import Offering
7
+ from pinecall.evals.runs import Runner
8
+ from pinecall.fleet.roster import Roster
9
+ from pinecall.gateway._served import ServedCalls, Serving
10
+ from pinecall.gateway._sockets import Sockets
11
+ from pinecall.gateway.calls.threads import Threads
12
+ from pinecall.log.logs import Logs
13
+ from pinecall.process.connections import Connections
14
+ from pinecall.process.metrics import Counters
15
+ from pinecall.retrieval.embed import Embedder
16
+ from pinecall.tenancy.codes import Codes
17
+ from pinecall.tenancy.knocks import Throttle
18
+ from pinecall.tenancy.mail import Outbox
19
+ from pinecall.tenancy.prompts import Prompts
20
+ from pinecall.tenancy.remembered import RememberedKeys
21
+ from pinecall.tenancy.signin import SignIns
22
+ from pinecall.tenancy.throttle import Window
23
+ from pinecall.tenancy.tokens import Signer
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Gateway:
28
+ """The gateway process: its connections, and the state it keeps in memory while it serves."""
29
+
30
+ connections: Connections
31
+ logs: Logs
32
+ sockets: Sockets
33
+ live: ServedCalls
34
+ roster: Roster
35
+ codes: Codes
36
+ # The keys verified lately, so a request costs no round trip to verify its key.
37
+ keys: RememberedKeys
38
+ signer: Signer
39
+ threads: Threads
40
+ # Set when the process is told to stop: every stream ends on it.
41
+ closing: asyncio.Event
42
+ # None when the providers row names no embedding, or the box holds no key for its vendor.
43
+ embedder: Embedder | None
44
+ evals: Runner
45
+ signins: SignIns
46
+ outbox: Outbox
47
+ # Thirty voice samples a minute per key: they cost vendor time and write no usage row.
48
+ samples: Throttle
49
+ # Each org's requests to each family of doors this minute, in each world.
50
+ paced: Window
51
+ # What GET /metrics reads: counted on the append path since the process started.
52
+ counters: Counters = field(default_factory=Counters)
53
+ prompts: Prompts = field(default_factory=Prompts)
54
+ # The loops that must run for the life of the process (the reaper, the sweep), by name: a loop
55
+ # that stopped on an error none of them catches is said on /metrics, for the alert.
56
+ loops: dict[str, asyncio.Task[None]] = field(default_factory=dict[str, asyncio.Task[None]])
57
+
58
+ @property
59
+ def offering(self) -> Offering:
60
+ """What sends a call to the worker this gateway chose."""
61
+ return Offering(
62
+ pool=self.connections.pool, servers=self.connections.servers, roster=self.roster
63
+ )
64
+
65
+ @property
66
+ def serving(self) -> Serving:
67
+ """What a call runs through."""
68
+ return Serving(
69
+ connections=self.connections,
70
+ logs=self.logs,
71
+ live=self.live,
72
+ embedder=self.embedder,
73
+ prompts=self.prompts,
74
+ counters=self.counters,
75
+ )