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.
- pinecall/__init__.py +0 -0
- pinecall/channels/__init__.py +0 -0
- pinecall/channels/_chooser.py +45 -0
- pinecall/channels/offers.py +219 -0
- pinecall/channels/rooms.py +176 -0
- pinecall/channels/routes.py +275 -0
- pinecall/channels/telephony/__init__.py +0 -0
- pinecall/channels/telephony/_twilio.py +349 -0
- pinecall/channels/telephony/carrier.py +229 -0
- pinecall/channels/telephony/carrier_catalog.py +92 -0
- pinecall/channels/telephony/carriers.csv +41 -0
- pinecall/channels/telephony/dialing.py +419 -0
- pinecall/channels/telephony/firewall.py +58 -0
- pinecall/channels/telephony/hand_over.py +78 -0
- pinecall/channels/telephony/number_path.py +176 -0
- pinecall/channels/telephony/numbers.py +530 -0
- pinecall/channels/telephony/origination.py +77 -0
- pinecall/channels/telephony/sip.py +448 -0
- pinecall/channels/whatsapp.py +413 -0
- pinecall/cli/__init__.py +0 -0
- pinecall/cli/_archive.py +85 -0
- pinecall/cli/_drift.py +38 -0
- pinecall/cli/_facts.py +67 -0
- pinecall/cli/_load.py +433 -0
- pinecall/cli/_operator.py +626 -0
- pinecall/cli/_sessions.py +164 -0
- pinecall/cli/_traceback.py +95 -0
- pinecall/cli/_usage.py +34 -0
- pinecall/cli/main.py +521 -0
- pinecall/domain/__init__.py +0 -0
- pinecall/domain/agent.py +446 -0
- pinecall/domain/call.py +152 -0
- pinecall/domain/country_codes.py +32 -0
- pinecall/domain/errors.py +151 -0
- pinecall/domain/names.py +115 -0
- pinecall/domain/org.py +120 -0
- pinecall/domain/person.py +208 -0
- pinecall/domain/scope.py +21 -0
- pinecall/evals/__init__.py +0 -0
- pinecall/evals/_evidence.py +158 -0
- pinecall/evals/calibration.py +112 -0
- pinecall/evals/callers.py +160 -0
- pinecall/evals/case.py +327 -0
- pinecall/evals/checks.py +437 -0
- pinecall/evals/compliance.py +197 -0
- pinecall/evals/dataset.py +451 -0
- pinecall/evals/goldens.py +331 -0
- pinecall/evals/judges.py +525 -0
- pinecall/evals/runs.py +252 -0
- pinecall/evals/spoken.py +356 -0
- pinecall/fleet/__init__.py +0 -0
- pinecall/fleet/client.py +569 -0
- pinecall/fleet/demand.py +51 -0
- pinecall/fleet/heartbeat.py +194 -0
- pinecall/fleet/measures.py +157 -0
- pinecall/fleet/roster.py +232 -0
- pinecall/fleet/worlds.py +44 -0
- pinecall/gateway/__init__.py +0 -0
- pinecall/gateway/_call_setup.py +53 -0
- pinecall/gateway/_deps.py +556 -0
- pinecall/gateway/_gateway.py +75 -0
- pinecall/gateway/_served.py +396 -0
- pinecall/gateway/_sockets.py +444 -0
- pinecall/gateway/_streams.py +83 -0
- pinecall/gateway/_text_calls.py +182 -0
- pinecall/gateway/api/__init__.py +0 -0
- pinecall/gateway/api/accounts.py +324 -0
- pinecall/gateway/api/agents.py +150 -0
- pinecall/gateway/api/apps.py +478 -0
- pinecall/gateway/api/box.py +658 -0
- pinecall/gateway/api/calibration.py +58 -0
- pinecall/gateway/api/callbacks.py +105 -0
- pinecall/gateway/api/calls.py +655 -0
- pinecall/gateway/api/chat.py +223 -0
- pinecall/gateway/api/dataset.py +163 -0
- pinecall/gateway/api/desk.py +113 -0
- pinecall/gateway/api/entries.py +77 -0
- pinecall/gateway/api/evals.py +647 -0
- pinecall/gateway/api/fleet.py +73 -0
- pinecall/gateway/api/hosting.py +241 -0
- pinecall/gateway/api/judges.py +97 -0
- pinecall/gateway/api/keys.py +110 -0
- pinecall/gateway/api/line.py +142 -0
- pinecall/gateway/api/members.py +197 -0
- pinecall/gateway/api/metrics.py +226 -0
- pinecall/gateway/api/numbers.py +476 -0
- pinecall/gateway/api/ops.py +428 -0
- pinecall/gateway/api/org.py +283 -0
- pinecall/gateway/api/personas.py +157 -0
- pinecall/gateway/api/pipeline.py +206 -0
- pinecall/gateway/api/prompts.py +50 -0
- pinecall/gateway/api/providers.py +240 -0
- pinecall/gateway/api/recordings.py +89 -0
- pinecall/gateway/api/relay.py +169 -0
- pinecall/gateway/api/retrieval.py +403 -0
- pinecall/gateway/api/runner.py +109 -0
- pinecall/gateway/api/sessions.py +102 -0
- pinecall/gateway/api/settings.py +463 -0
- pinecall/gateway/api/signup.py +164 -0
- pinecall/gateway/api/sso_login.py +168 -0
- pinecall/gateway/api/threads.py +187 -0
- pinecall/gateway/api/usage.py +303 -0
- pinecall/gateway/api/visitors.py +222 -0
- pinecall/gateway/api/whatsapp.py +48 -0
- pinecall/gateway/api/widget.py +49 -0
- pinecall/gateway/app.py +504 -0
- pinecall/gateway/calls/__init__.py +0 -0
- pinecall/gateway/calls/binding.py +63 -0
- pinecall/gateway/calls/commands.py +18 -0
- pinecall/gateway/calls/inbox.py +100 -0
- pinecall/gateway/calls/known.py +23 -0
- pinecall/gateway/calls/owners.py +87 -0
- pinecall/gateway/calls/pump.py +33 -0
- pinecall/gateway/calls/serving.py +118 -0
- pinecall/gateway/calls/threads.py +367 -0
- pinecall/gateway/dispatching/__init__.py +0 -0
- pinecall/gateway/dispatching/arrivals.py +58 -0
- pinecall/gateway/dispatching/sweep.py +43 -0
- pinecall/gateway/ending/__init__.py +0 -0
- pinecall/gateway/ending/reaper.py +160 -0
- pinecall/gateway/ending/seal.py +312 -0
- pinecall/gateway/ending/stranded.py +117 -0
- pinecall/log/__init__.py +0 -0
- pinecall/log/_histogram.py +74 -0
- pinecall/log/_relay.py +259 -0
- pinecall/log/_writer.py +454 -0
- pinecall/log/days.py +154 -0
- pinecall/log/drift.py +550 -0
- pinecall/log/facts.py +358 -0
- pinecall/log/inbox.py +169 -0
- pinecall/log/lists.py +155 -0
- pinecall/log/logs.py +581 -0
- pinecall/log/openings.py +71 -0
- pinecall/log/private.py +135 -0
- pinecall/log/queries.py +229 -0
- pinecall/log/readers.py +207 -0
- pinecall/log/reduce.py +659 -0
- pinecall/log/refold.py +217 -0
- pinecall/log/store.py +370 -0
- pinecall/log/usage.py +242 -0
- pinecall/postgres/__init__.py +0 -0
- pinecall/postgres/migrate.py +202 -0
- pinecall/postgres/migrations/0001_schema.sql +591 -0
- pinecall/postgres/migrations/0002_worlds.sql +15 -0
- pinecall/postgres/migrations/0003_lent.sql +4 -0
- pinecall/postgres/migrations/0004_carriers.sql +23 -0
- pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
- pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
- pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
- pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
- pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
- pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
- pinecall/postgres/migrations/0011_erasure.sql +32 -0
- pinecall/postgres/migrations/0012_org_policy.sql +10 -0
- pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
- pinecall/postgres/migrations/0014_consents.sql +24 -0
- pinecall/postgres/migrations/0015_disclosure.sql +7 -0
- pinecall/postgres/migrations/0016_call_records.sql +21 -0
- pinecall/postgres/migrations/0017_reads.sql +17 -0
- pinecall/postgres/migrations/0018_hosting.sql +41 -0
- pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
- pinecall/postgres/migrations/0020_written_once.sql +7 -0
- pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
- pinecall/postgres/migrations/0022_sealed.sql +39 -0
- pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
- pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
- pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
- pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
- pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
- pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
- pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
- pinecall/postgres/migrations/0060_drift.sql +62 -0
- pinecall/postgres/migrations/0061_canaries.sql +24 -0
- pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
- pinecall/postgres/migrations/0063_prompts.sql +16 -0
- pinecall/postgres/migrations/0075_call_private.sql +14 -0
- pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
- pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
- pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
- pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
- pinecall/postgres/migrations/0080_call_openings.sql +20 -0
- pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
- pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
- pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
- pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
- pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
- pinecall/postgres/migrations/0086_route_origin.sql +17 -0
- pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
- pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
- pinecall/postgres/migrations/0089_offers.sql +21 -0
- pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
- pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
- pinecall/postgres/migrations/0092_key_parent.sql +6 -0
- pinecall/postgres/migrations/0093_code_tries.sql +10 -0
- pinecall/postgres/migrations/0094_route_approval.sql +12 -0
- pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
- pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
- pinecall/postgres/migrations/0097_case_status.sql +19 -0
- pinecall/postgres/pool.py +190 -0
- pinecall/process/__init__.py +0 -0
- pinecall/process/_objects.py +97 -0
- pinecall/process/box_settings.py +23 -0
- pinecall/process/connections.py +124 -0
- pinecall/process/metrics.py +127 -0
- pinecall/process/recordings.py +323 -0
- pinecall/process/resolver.py +41 -0
- pinecall/process/sealed_audio.py +173 -0
- pinecall/process/settings.py +534 -0
- pinecall/process/shared.py +158 -0
- pinecall/process/signal.py +416 -0
- pinecall/providers/__init__.py +0 -0
- pinecall/providers/build.py +570 -0
- pinecall/providers/catalog.py +266 -0
- pinecall/providers/credentials.py +230 -0
- pinecall/providers/declared.py +69 -0
- pinecall/providers/prices.py +240 -0
- pinecall/providers/tried.py +104 -0
- pinecall/providers/voices.py +174 -0
- pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
- pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
- pinecall/public/console/favicon.png +0 -0
- pinecall/public/console/index.html +20 -0
- pinecall/public/console/logo-mark.png +0 -0
- pinecall/public/console/pinecall-logo-dark.png +0 -0
- pinecall/public/console/pinecall-logo.png +0 -0
- pinecall/public/console/pinecall-mark.png +0 -0
- pinecall/public/console/sw.js +37 -0
- pinecall/public/widget/pinecall-widget.js +853 -0
- pinecall/py.typed +0 -0
- pinecall/retrieval/__init__.py +0 -0
- pinecall/retrieval/_search.py +186 -0
- pinecall/retrieval/_sections.py +70 -0
- pinecall/retrieval/embed.py +340 -0
- pinecall/retrieval/extraction.py +403 -0
- pinecall/retrieval/knowledge.py +659 -0
- pinecall/retrieval/lookups.py +192 -0
- pinecall/retrieval/memory.py +603 -0
- pinecall/runner/__init__.py +0 -0
- pinecall/runner/_kube.py +459 -0
- pinecall/runner/_plan.py +157 -0
- pinecall/runner/main.py +306 -0
- pinecall/session/__init__.py +0 -0
- pinecall/session/_agent.py +141 -0
- pinecall/session/_hearing.py +101 -0
- pinecall/session/_livekit.py +195 -0
- pinecall/session/_prompt.py +147 -0
- pinecall/session/_recording.py +237 -0
- pinecall/session/call.py +367 -0
- pinecall/session/clock.py +40 -0
- pinecall/session/hold.py +239 -0
- pinecall/session/room.py +626 -0
- pinecall/session/session.py +690 -0
- pinecall/session/text.py +144 -0
- pinecall/session/tools.py +483 -0
- pinecall/session/voice.py +118 -0
- pinecall/session/widget.py +201 -0
- pinecall/tenancy/__init__.py +0 -0
- pinecall/tenancy/admission.py +204 -0
- pinecall/tenancy/agents.py +164 -0
- pinecall/tenancy/canary.py +126 -0
- pinecall/tenancy/carrier_networks.py +156 -0
- pinecall/tenancy/carriers.py +265 -0
- pinecall/tenancy/codes.py +196 -0
- pinecall/tenancy/consents.py +167 -0
- pinecall/tenancy/dial_policy.py +405 -0
- pinecall/tenancy/disclosure.py +41 -0
- pinecall/tenancy/erasure.py +204 -0
- pinecall/tenancy/export.py +167 -0
- pinecall/tenancy/hosted_running.py +166 -0
- pinecall/tenancy/hosting.py +475 -0
- pinecall/tenancy/judges.py +126 -0
- pinecall/tenancy/keys.py +467 -0
- pinecall/tenancy/knocks.py +40 -0
- pinecall/tenancy/letters.py +378 -0
- pinecall/tenancy/mail.py +458 -0
- pinecall/tenancy/org_secrets.py +123 -0
- pinecall/tenancy/orgs.py +78 -0
- pinecall/tenancy/people.py +543 -0
- pinecall/tenancy/personas.py +145 -0
- pinecall/tenancy/policy.py +65 -0
- pinecall/tenancy/prompts.py +48 -0
- pinecall/tenancy/reads.py +73 -0
- pinecall/tenancy/recording_keys.py +41 -0
- pinecall/tenancy/remembered.py +148 -0
- pinecall/tenancy/retention.py +86 -0
- pinecall/tenancy/scopes.py +406 -0
- pinecall/tenancy/signin.py +622 -0
- pinecall/tenancy/spend.py +69 -0
- pinecall/tenancy/sso.py +431 -0
- pinecall/tenancy/sso_domains.py +132 -0
- pinecall/tenancy/throttle.py +88 -0
- pinecall/tenancy/tokens.py +400 -0
- pinecall/tenancy/traceback.py +70 -0
- pinecall/tenancy/usage.py +136 -0
- pinecall/tenancy/vault.py +204 -0
- pinecall/tenancy/words.py +144 -0
- pinecall/wire/__init__.py +0 -0
- pinecall/wire/commands.py +350 -0
- pinecall/wire/events.py +655 -0
- pinecall/wire/frames.py +65 -0
- pinecall/wire/metrics.py +287 -0
- pinecall/wire/parts.py +320 -0
- pinecall/wire/rest/__init__.py +0 -0
- pinecall/wire/rest/accounts.py +512 -0
- pinecall/wire/rest/agents.py +172 -0
- pinecall/wire/rest/calls.py +397 -0
- pinecall/wire/rest/evals.py +453 -0
- pinecall/wire/rest/fleet.py +69 -0
- pinecall/wire/rest/hosting.py +174 -0
- pinecall/wire/rest/numbers.py +280 -0
- pinecall/wire/rest/ops.py +342 -0
- pinecall/wire/rest/providers.py +83 -0
- pinecall/wire/rest/retrieval.py +301 -0
- pinecall/wire/rest/settings.py +184 -0
- pinecall/wire/rest/usage.py +245 -0
- pinecall/wire/room.py +61 -0
- pinecall/wire/scores.py +49 -0
- pinecall/wire/state.py +243 -0
- pinecall/worker/__init__.py +0 -0
- pinecall/worker/_job.py +655 -0
- pinecall/worker/_recorder.py +82 -0
- pinecall/worker/_traces.py +48 -0
- pinecall/worker/main.py +307 -0
- pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
- pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
- pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
- pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|