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,428 @@
1
+ """The box's own doors over its orgs: the box, the orgs, quotas, people, keys, tracebacks."""
2
+
3
+ from dataclasses import replace
4
+ from importlib.metadata import version
5
+ from typing import Annotated
6
+
7
+ from fastapi import APIRouter, Depends, Path, Request
8
+
9
+ from pinecall.channels import routes
10
+ from pinecall.domain.errors import Conflict, NotFound
11
+ from pinecall.domain.names import DISTRIBUTION, ENVS, Env, parse_slug
12
+ from pinecall.domain.org import Org, Quotas
13
+ from pinecall.domain.person import KEY_SCOPES, key_scopes, parse_role
14
+ from pinecall.gateway._deps import GatewayDep, bearer_of, operator, public_url
15
+ from pinecall.gateway._gateway import Gateway
16
+ from pinecall.gateway.api.keys import key_row
17
+ from pinecall.gateway.api.org import sso_row
18
+ from pinecall.gateway.api.providers import credentials_of, installed_vendor
19
+ from pinecall.process.recordings import recordings_of
20
+ from pinecall.providers import build
21
+ from pinecall.providers.credentials import parse_lending
22
+ from pinecall.retrieval import knowledge, memory
23
+ from pinecall.tenancy import (
24
+ admission,
25
+ dial_policy,
26
+ erasure,
27
+ keys,
28
+ letters,
29
+ orgs,
30
+ people,
31
+ reads,
32
+ sso,
33
+ sso_domains,
34
+ traceback,
35
+ vault,
36
+ )
37
+ from pinecall.tenancy.dial_policy import Guards
38
+ from pinecall.tenancy.letters import Link
39
+ from pinecall.wire.rest.accounts import (
40
+ BoxIdentityResponse,
41
+ InvitationResponse,
42
+ InviteMemberRequest,
43
+ KeyIssuedResponse,
44
+ KeyRow,
45
+ MemberRow,
46
+ OrgSsoResponse,
47
+ RevokeKeyResponse,
48
+ )
49
+ from pinecall.wire.rest.numbers import DialGuards
50
+ from pinecall.wire.rest.ops import (
51
+ AgentMovedResponse,
52
+ CreateOrgRequest,
53
+ IssueKeyRequest,
54
+ MoveAgentRequest,
55
+ OperatorRequest,
56
+ OrgHolding,
57
+ OrgMembersResponse,
58
+ OrgProfile,
59
+ OrgQuotas,
60
+ OrgRow,
61
+ PutDiallingRequest,
62
+ PutQuotasRequest,
63
+ SsoRequiredRequest,
64
+ Traceback,
65
+ )
66
+ from pinecall.wire.rest.providers import ProviderKeyRequest, VendorsResponse
67
+
68
+ router = APIRouter(dependencies=[Depends(operator)])
69
+
70
+
71
+ NO_SUCH_ORG = "no org named {named}: by id or by slug"
72
+
73
+
74
+ # Forgetting an org never cascades over what still points at it.
75
+ STILL_IN_USE = "org {slug} still has {what}: revoke its keys and remove its routes first"
76
+
77
+
78
+ NOT_HELD = "agent {slug} is held right now: stop it, move it, and start it again"
79
+
80
+
81
+ NO_SUCH_AGENT = "no agent named {slug} has ever written a log here"
82
+
83
+
84
+ NO_SUCH_KEY = "no live key with fingerprint {fingerprint}"
85
+
86
+
87
+ NO_SSO = "org {slug} signs in with no identity provider"
88
+
89
+
90
+ NO_SUCH_VENDOR_KEY = "org {slug} brought no key for {vendor}"
91
+
92
+
93
+ # Who a letter says invited, when the box's own key did.
94
+ THE_OPERATOR = "The operator"
95
+
96
+
97
+ MemberId = Annotated[str, Path(alias="id")]
98
+
99
+
100
+ # The path says {id}, as the console spells it; `id` is a builtin, so the function says member.
101
+ A_MEMBER = "/v1/ops/orgs/{named}/members/{id}"
102
+
103
+
104
+ # The console opens its Box screens when this answers 200; anything else hides them.
105
+ @router.get("/v1/ops/whoami")
106
+ async def box_identity(request: Request, gateway: GatewayDep) -> BoxIdentityResponse:
107
+ """The box this key opens, and the person holding it; nobody for the box's own key."""
108
+ data = bearer_of(request.headers)
109
+ found = None if data is None else await gateway.keys.verify(data)
110
+ person = None if found is None else found.member
111
+ return BoxIdentityResponse(
112
+ operator=True,
113
+ version=version(DISTRIBUTION),
114
+ domain=gateway.connections.settings.domain,
115
+ name=None if person is None else person.name,
116
+ org=None if person is None else person.org,
117
+ )
118
+
119
+
120
+ @router.get("/v1/ops/orgs")
121
+ async def list_orgs(gateway: GatewayDep) -> list[OrgRow]:
122
+ """Every org, oldest first, the default one first."""
123
+ return [_org_row(org) for org in await orgs.listed(gateway.connections.pool)]
124
+
125
+
126
+ @router.post("/v1/ops/orgs", status_code=201)
127
+ async def create_org(body: CreateOrgRequest, gateway: GatewayDep) -> OrgRow:
128
+ """A new org, its id minted here, born with what admission gives one."""
129
+ slug = parse_slug(body.slug.strip())
130
+ return _org_row(await orgs.create(gateway.connections.pool, slug, body.name or slug))
131
+
132
+
133
+ # `holding` is what the org has now, never a fold of the log: usage is /v1/ops/usage.
134
+ @router.get("/v1/ops/orgs/{named}")
135
+ async def org_standing(named: str, gateway: GatewayDep) -> OrgProfile:
136
+ """One org as it stands: its quotas per world, its dial guards, and what it holds."""
137
+ pool = gateway.connections.pool
138
+ org = await _org(gateway, named)
139
+ quotas: dict[Env, Quotas] = {env: await admission.quotas_of(pool, org.id, env) for env in ENVS}
140
+ return OrgProfile(
141
+ id=org.id,
142
+ slug=org.slug,
143
+ name=org.name,
144
+ quotas={env: _quotas_row(kept) for env, kept in quotas.items()},
145
+ dialling=_guards_row(await dial_policy.guards_of(pool, org.id)),
146
+ holding=OrgHolding(
147
+ memory_facts=sum([await memory.kept(pool, org.id, env) for env in ENVS]),
148
+ knowledge_chunks=sum([await knowledge.kept(pool, org.id, env) for env in ENVS]),
149
+ numbers=sum([await routes.managed_in(pool, org.id, env) for env in ENVS]),
150
+ seats=await people.seated(pool, org.id),
151
+ ),
152
+ )
153
+
154
+
155
+ @router.delete("/v1/ops/orgs/{named}", status_code=204)
156
+ async def remove_org(named: str, gateway: GatewayDep) -> None:
157
+ """Erase the org whole, its calls and recordings too; refused while a key or route names it."""
158
+ pool = gateway.connections.pool
159
+ org = await _org(gateway, named)
160
+ if any(row.revoked_at is None for row in await keys.listed(pool, org.id)):
161
+ raise Conflict(STILL_IN_USE.format(slug=org.slug, what="live keys"))
162
+ for env in ENVS:
163
+ if await routes.of_org(pool, org.id, env):
164
+ raise Conflict(STILL_IN_USE.format(slug=org.slug, what="routes"))
165
+ recordings = recordings_of(gateway.connections.settings, gateway.connections.http)
166
+ erased = await erasure.org(pool, recordings, org.id, by=reads.OPERATOR)
167
+ for call in erased.calls:
168
+ gateway.logs.forget(call)
169
+
170
+
171
+ # The way back from an agent registered with the wrong org's key: its logs and its numbers move.
172
+ # The operator's, since an org able to pull a slug could take another org's agent.
173
+ @router.put("/v1/ops/orgs/{named}/agents")
174
+ async def move_agent(named: str, body: MoveAgentRequest, gateway: GatewayDep) -> AgentMovedResponse:
175
+ """An agent's logs and numbers moved into this org; refused while somebody holds it."""
176
+ pool = gateway.connections.pool
177
+ org = await _org(gateway, named)
178
+ if gateway.sockets.declared(body.agent) is not None:
179
+ raise Conflict(NOT_HELD.format(slug=body.agent))
180
+ moved = await gateway.logs.store.moved(body.agent, org.id)
181
+ if moved == 0:
182
+ raise NotFound(NO_SUCH_AGENT.format(slug=body.agent))
183
+ numbers, stayed = await routes.moved_with_agent(pool, body.agent, org.id)
184
+ return AgentMovedResponse(
185
+ agent=body.agent, org=org.slug, logs=moved, numbers=numbers, stayed=stayed
186
+ )
187
+
188
+
189
+ @router.put("/v1/ops/orgs/{named}/quotas")
190
+ async def put_quotas(named: str, body: PutQuotasRequest, gateway: GatewayDep) -> OrgQuotas:
191
+ """The org's limits in one world, replaced whole; they bite the next call and register."""
192
+ org = await _org(gateway, named)
193
+ wanted = body.quotas
194
+ quotas = Quotas(
195
+ minutes=wanted.limits.get("minutes"),
196
+ messages=wanted.limits.get("messages"),
197
+ agents=wanted.limits.get("agents"),
198
+ concurrent_calls=wanted.limits.get("concurrent_calls"),
199
+ memory_facts=wanted.limits.get("memory_facts"),
200
+ knowledge_chunks=wanted.limits.get("knowledge_chunks"),
201
+ numbers=wanted.limits.get("numbers"),
202
+ seats=wanted.limits.get("seats"),
203
+ llm_tokens=wanted.limits.get("llm_tokens"),
204
+ hosted_apps=wanted.limits.get("hosted_apps"),
205
+ budget_usd=wanted.budget_usd,
206
+ lends=None if wanted.lends is None else parse_lending(wanted.lends),
207
+ )
208
+ await admission.set_quotas(gateway.connections.pool, org.id, body.env, quotas)
209
+ return _quotas_row(quotas)
210
+
211
+
212
+ # The operator's: an org able to lift its own dial guards would have none.
213
+ @router.put("/v1/ops/orgs/{named}/dialling")
214
+ async def put_dialling(named: str, body: PutDiallingRequest, gateway: GatewayDep) -> DialGuards:
215
+ """The org's dial guards, replaced whole; one left out is the default."""
216
+ org = await _org(gateway, named)
217
+ guards = Guards.model_validate(body.model_dump(exclude_none=True))
218
+ await dial_policy.put_guards(gateway.connections.pool, org.id, guards)
219
+ return _guards_row(guards)
220
+
221
+
222
+ # ── its people ──
223
+
224
+
225
+ @router.get("/v1/ops/orgs/{named}/members")
226
+ async def org_members(named: str, gateway: GatewayDep) -> OrgMembersResponse:
227
+ """The org's people, oldest first, and how many hold a seat."""
228
+ pool = gateway.connections.pool
229
+ org = await _org(gateway, named)
230
+ return OrgMembersResponse(
231
+ members=[MemberRow.of(member) for member in await people.listed(pool, org.id)],
232
+ seated=await people.seated(pool, org.id),
233
+ )
234
+
235
+
236
+ # Takes no seat of the org's plan: this is how an org gets its first admin with sign-up shut.
237
+ # The token is answered this once; the letter goes too when the box can mail.
238
+ @router.post("/v1/ops/orgs/{named}/members", status_code=201)
239
+ async def invite_member(
240
+ named: str, body: InviteMemberRequest, request: Request, gateway: GatewayDep
241
+ ) -> InvitationResponse:
242
+ """Invite a person into the org, the link's token once."""
243
+ pool = gateway.connections.pool
244
+ org = await _org(gateway, named)
245
+ invitee = people.Invitee(
246
+ body.email, body.name, parse_role(body.role), frozenset(body.agents), body.production
247
+ )
248
+ invited = await people.invite(pool, org.id, invitee, seats=None, vouched=True)
249
+ card = letters.card_link(public_url(request, gateway), invited.token)
250
+ link = Link(org=org.name, link=card, by=THE_OPERATOR, dies=invited.expires_at)
251
+ letter = letters.invitation_letter(invited.member.email, link, await letters.brand_of(pool))
252
+ mailed = await gateway.outbox.post(None, letter)
253
+ return InvitationResponse(
254
+ member=MemberRow.of(invited.member),
255
+ token=invited.token,
256
+ expires_at=invited.expires_at,
257
+ mailed=mailed,
258
+ link=card,
259
+ )
260
+
261
+
262
+ # Here and not on the tenant's door: an org admin could otherwise make itself the operator.
263
+ @router.put(f"{A_MEMBER}/operator")
264
+ async def make_operator(
265
+ named: str, member: MemberId, body: OperatorRequest, gateway: GatewayDep
266
+ ) -> MemberRow:
267
+ """Whether this member runs the box; false takes it back at once."""
268
+ org = await _org(gateway, named)
269
+ changed = await people.make_operator(gateway.connections.pool, org.id, member, on=body.operator)
270
+ gateway.keys.forget(subject=member)
271
+ if changed is None:
272
+ raise NotFound(people.NOBODY_BY_THAT_ID)
273
+ return MemberRow.of(changed)
274
+
275
+
276
+ # The tenant door's rules less "not yourself": the last active admin still stays.
277
+ @router.delete(A_MEMBER, status_code=204)
278
+ async def remove_member(named: str, member: MemberId, gateway: GatewayDep) -> None:
279
+ """The person out of the org for good, their keys revoked."""
280
+ org = await _org(gateway, named)
281
+ await people.remove(gateway.connections.pool, org.id, member)
282
+ gateway.keys.forget(subject=member)
283
+
284
+
285
+ @router.get("/v1/ops/orgs/{named}/sso")
286
+ async def org_sso(named: str, request: Request, gateway: GatewayDep) -> OrgSsoResponse:
287
+ """Which provider the org signs in with, never its secret."""
288
+ connections = gateway.connections
289
+ org = await _org(gateway, named)
290
+ wired = await sso.sso_of(connections.pool, connections.vault, org.id)
291
+ proofs = await sso_domains.of_org(connections.pool, org.id)
292
+ return sso_row(wired, f"{public_url(request, gateway)}{sso.CALLBACK}", proofs)
293
+
294
+
295
+ # The break-glass: a password opens the org again while its provider is down. Never the other
296
+ # way: requiring the provider is the org's own call.
297
+ @router.put("/v1/ops/orgs/{named}/sso/required")
298
+ async def sso_required(
299
+ named: str, body: SsoRequiredRequest, request: Request, gateway: GatewayDep
300
+ ) -> OrgSsoResponse:
301
+ """Whether a password may still open the org beside its provider."""
302
+ connections = gateway.connections
303
+ org = await _org(gateway, named)
304
+ wired = await sso.sso_of(connections.pool, connections.vault, org.id)
305
+ if wired is None:
306
+ raise NotFound(NO_SSO.format(slug=org.slug))
307
+ changed = replace(wired, required=body.required)
308
+ await sso.put_sso(connections.pool, connections.vault, changed)
309
+ proofs = await sso_domains.of_org(connections.pool, org.id)
310
+ return sso_row(changed, f"{public_url(request, gateway)}{sso.CALLBACK}", proofs)
311
+
312
+
313
+ # ── its keys ──
314
+
315
+
316
+ @router.get("/v1/ops/orgs/{named}/keys")
317
+ async def org_keys(named: str, gateway: GatewayDep) -> list[KeyRow]:
318
+ """Every key of the org, oldest first, the revoked ones named as revoked; never a key."""
319
+ pool = gateway.connections.pool
320
+ org = await _org(gateway, named)
321
+ names = {member.id: member.name for member in await people.listed(pool, org.id)}
322
+ return [key_row(row, names) for row in await keys.listed(pool, org.id)]
323
+
324
+
325
+ # The one answer that carries the key: only its fingerprint is kept.
326
+ @router.post("/v1/ops/orgs/{named}/keys")
327
+ async def issue_key(named: str, body: IssueKeyRequest, gateway: GatewayDep) -> KeyIssuedResponse:
328
+ """A key of the org in the world named, answered this once."""
329
+ org = await _org(gateway, named)
330
+ scopes = KEY_SCOPES if body.scopes is None else key_scopes(body.scopes)
331
+ issued = keys.Issued(
332
+ org=org.id,
333
+ env=body.env,
334
+ scopes=scopes,
335
+ label=body.label,
336
+ subject=body.subject,
337
+ name=body.name,
338
+ )
339
+ minted, secret = await keys.issue(gateway.connections.pool, issued)
340
+ return KeyIssuedResponse.of(minted, secret)
341
+
342
+
343
+ # A POST, not a DELETE: the row stays, so the entries that name the key still read.
344
+ @router.post("/v1/ops/keys/{fingerprint}/revoke")
345
+ async def revoke_key(fingerprint: str, gateway: GatewayDep) -> RevokeKeyResponse:
346
+ """One key stops opening anything from the next request on."""
347
+ stopped = await keys.revoke(gateway.connections.pool, fingerprint)
348
+ if not stopped:
349
+ raise NotFound(NO_SUCH_KEY.format(fingerprint=fingerprint))
350
+ for each in stopped:
351
+ gateway.keys.forget(fingerprint=each)
352
+ return RevokeKeyResponse(fingerprint=fingerprint, revoked=True)
353
+
354
+
355
+ # ── its vendor keys ──
356
+
357
+
358
+ @router.get("/v1/ops/orgs/{named}/provider-keys")
359
+ async def org_vendors(named: str, gateway: GatewayDep) -> VendorsResponse:
360
+ """The vendors the org brought its own credentials for, never the credentials."""
361
+ org = await _org(gateway, named)
362
+ return VendorsResponse(vendors=await vault.vendors_of(gateway.connections.pool, org.id))
363
+
364
+
365
+ @router.put("/v1/ops/orgs/{named}/provider-keys/{vendor}", status_code=204)
366
+ async def put_org_vendor_key(
367
+ named: str, vendor: str, body: ProviderKeyRequest, gateway: GatewayDep
368
+ ) -> None:
369
+ """The org's own credentials for a vendor, set for it; its calls run on them next."""
370
+ connections = gateway.connections
371
+ org = await _org(gateway, named)
372
+ named_vendor, credentials = installed_vendor(vendor), credentials_of(body)
373
+ build.check_credentials(named_vendor, credentials)
374
+ await vault.put_credentials(
375
+ connections.pool, connections.vault, org.id, named_vendor, credentials
376
+ )
377
+
378
+
379
+ @router.delete("/v1/ops/orgs/{named}/provider-keys/{vendor}", status_code=204)
380
+ async def drop_org_vendor_key(named: str, vendor: str, gateway: GatewayDep) -> None:
381
+ """The org's credentials for a vendor taken back; its calls run on the box's next."""
382
+ org = await _org(gateway, named)
383
+ named_vendor = installed_vendor(vendor)
384
+ if not await vault.drop_credentials(gateway.connections.pool, org.id, named_vendor):
385
+ raise NotFound(NO_SUCH_VENDOR_KEY.format(slug=org.slug, vendor=named_vendor))
386
+
387
+
388
+ # A carrier asks within days of a call; the records reach 24 months back, the default here.
389
+ @router.get("/v1/ops/traceback")
390
+ async def number_traceback(
391
+ gateway: GatewayDep, number: str, since: float | None = None
392
+ ) -> Traceback:
393
+ """Every phone call with a number, kept or erased, and every dial to it, of every org."""
394
+ pool = gateway.connections.pool
395
+ found = await traceback.of_number(pool, number, since)
396
+ await traceback.read_by(pool, found, reads.OPERATOR)
397
+ return found
398
+
399
+
400
+ def _org_row(org: Org) -> OrgRow:
401
+ """An org as the doors send it."""
402
+ return OrgRow(id=org.id, slug=org.slug, name=org.name)
403
+
404
+
405
+ def _quotas_row(quotas: Quotas) -> OrgQuotas:
406
+ """An org's limits as the doors send them, `lends` sorted."""
407
+ return OrgQuotas(
408
+ limits=dict(quotas.limits),
409
+ budget_usd=quotas.budget_usd,
410
+ lends=None if quotas.lends is None else sorted(quotas.lends),
411
+ )
412
+
413
+
414
+ def _guards_row(guards: Guards) -> DialGuards:
415
+ """The dial guards as the doors send them."""
416
+ return DialGuards(
417
+ dial_anywhere=guards.dial_anywhere,
418
+ per_minute=guards.per_minute,
419
+ per_day=guards.per_day,
420
+ max_duration_s=guards.max_duration_s,
421
+ )
422
+
423
+
424
+ async def _org(gateway: Gateway, named: str) -> Org:
425
+ org = await orgs.find(gateway.connections.pool, named)
426
+ if org is None:
427
+ raise NotFound(NO_SUCH_ORG.format(named=named))
428
+ return org