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,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
|