cortad 0.3.10 → 0.4.0

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.
package/lib/proc.mjs CHANGED
@@ -58,7 +58,9 @@ export async function listenerOn(port) {
58
58
  // already in use", Go's "0.0.0.0:8080: bind: address already in use", Node's "EADDRINUSE: address
59
59
  // already in use :::3000" or "... 127.0.0.1:3000", Rails' "... for "127.0.0.1" port 3000". 0 when
60
60
  // it names none.
61
- export const portInError = (said) => Number(/[:\s(,](\d{4,5})\)?:?[^\n\d]{0,12}address already in use/i.exec(said)?.[1] ?? /(?:EADDRINUSE|address already in use)[^\n]{0,60}?[:\s](\d{4,5})\b/i.exec(said)?.[1] ?? 0);
61
+ // The port in the app's own words for a taken one, however its runtime spells the address: Node's
62
+ // "EADDRINUSE: address already in use :::3100", uvicorn's "('127.0.0.1', 58889): [errno 48] address already in use".
63
+ export const portInError = (said) => Number(/[:\s(,](\d{4,5})\)?:?[^\n]{0,40}?address already in use/i.exec(said)?.[1] ?? /(?:EADDRINUSE|address already in use)[^\n]{0,60}?[:\s](\d{4,5})\b/i.exec(said)?.[1] ?? 0);
62
64
 
63
65
  // Whoever holds a port: its pid, the program and its first argument (never the rest of its command
64
66
  // line, which can carry a key), and the folder it runs in.
package/lib/proof.mjs CHANGED
@@ -991,7 +991,14 @@ export function canaryOf(exchanges, proof, found = null, own = []) {
991
991
  if (carried(b1)) add("shared-history", "theirs", "A new conversation carried an earlier one's words: every conversation on this endpoint shares what your app keeps, so test conversations would read each other's.");
992
992
  const met = metIn(a1, own);
993
993
  if (met) add("unreadable", "theirs", met);
994
- else if (!ok(a1.status) || !replyOf(a1).text.trim()) add("unreadable", ok(a1.status) ? "ours" : "theirs", ok(a1.status) ? "The reply to the first message could not be read as words." : `Your app answered ${a1.status} to the first message.`);
994
+ else if (!ok(a1.status)) add("unreadable", "theirs", `Your app answered ${a1.status} to the first message.`);
995
+ else if (!replyOf(a1).text.trim()) {
996
+ // No words because the app's own model refused it is the app's key, card or model, never our
997
+ // reading: a wrong key in the twin's env file held Run as "could not be read as words", ours.
998
+ const calls = generation(a1);
999
+ const refused = calls.length && calls.every((c) => REFUSED.has(Number(c.status))) ? calls.at(-1) : null;
1000
+ add("unreadable", refused ? "theirs" : "ours", refused ? `The first message reached your app's model, and the ${modelCall(refused)}, so no words came back.` : "The reply to the first message could not be read as words.");
1001
+ }
995
1002
  const instructions = str(generation(a1).find((c) => typeof c.instructions === "string")?.instructions).trim();
996
1003
  const pieces = piecesOf(instructions);
997
1004
  const request = norm(a1.body);
package/lib/proxy.mjs CHANGED
@@ -34,12 +34,23 @@ const asked = (raw) => (/^[^=&\s]+=[^&]*(?:&[^=&\s]+=[^&]*)*$/.test(raw) ? [...n
34
34
  // ponytail: a worker slower than this gets a request said unseen; the wait per app if one is seen.
35
35
  const UNSEEN_MS = 3000;
36
36
 
37
- const listen = async (server) => { server.listen(0, "127.0.0.1"); await once(server, "listening"); return server.address().port; };
37
+ // On the port asked for when it is free, else on any: a container's settings name the model port once.
38
+ const listen = async (server, port = 0) => {
39
+ const at = await new Promise((resolve) => {
40
+ server.once("error", () => resolve(null));
41
+ server.listen(port, "127.0.0.1", () => resolve(server.address().port));
42
+ });
43
+ if (at !== null) return at;
44
+ server.removeAllListeners("error");
45
+ server.listen(0, "127.0.0.1");
46
+ await once(server, "listening");
47
+ return server.address().port;
48
+ };
38
49
 
39
50
  // `file`: the trace file the command reads. `target()`: where the app answers now, { host, port };
40
51
  // it moves when the app is started again on another port. `onSeen`: the first model call came through.
41
52
  // `onUnseen(door)`: a request the app answered in words with no model call begun, said once, as "POST /chat".
42
- export async function openProxy({ file, rulesFile, target, onSeen = () => {}, onUnseen = () => {} }) {
53
+ export async function openProxy({ file, rulesFile, target, modelPort: wantedModelPort = 0, onSeen = () => {}, onUnseen = () => {} }) {
43
54
  const row = (value) => { try { appendFileSync(file, JSON.stringify(scrubbed(value)) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ } };
44
55
  const rec = makeRecorder({ row, rulesFile });
45
56
  // The requests in flight through the front that a model call can be made for.
@@ -156,7 +167,7 @@ export async function openProxy({ file, rulesFile, target, onSeen = () => {}, on
156
167
  const model = createServer((req, res) => { relay(req, res).catch(() => res.destroy()); });
157
168
 
158
169
  const frontPort = await listen(front);
159
- const modelPort = await listen(model);
170
+ const modelPort = await listen(model, wantedModelPort);
160
171
  row({ hello: "proxy", pid: process.pid });
161
172
  const via = (upstream) => {
162
173
  if (!slots.includes(upstream)) slots.push(upstream);
@@ -11,9 +11,6 @@ _FILE = os.environ.get("CORTAD_TRACE_FILE")
11
11
  _RULES = os.environ.get("CORTAD_RULES_FILE")
12
12
  _WRITES = os.environ.get("CORTAD_WRITES_DIR")
13
13
  _APP_ROOT = os.environ.get("CORTAD_APP_ROOT")
14
- _OUTBOUND = os.environ.get("CORTAD_OUTBOUND_FILE")
15
- # The port this hook answers held writes on: its own server, never a store the app reached.
16
- _held_ports = []
17
14
 
18
15
 
19
16
  # What the app writes into its own folder while a run is on: each file copied once before its first
@@ -1272,388 +1269,6 @@ def _install():
1272
1269
  if req and is_model_call(url):
1273
1270
  req["heard"] = True
1274
1271
 
1275
- # Outbound writes a trial makes are held in the app, never sent: a trial that books, charges,
1276
- # emails or deletes would otherwise do it for real. Held: a call made inside a request the run
1277
- # tagged, with a writing method, to a host that is not a model, a retrieval, this machine, a copy
1278
- # of ours or a host the person said yes to. A call carrying a provider's test key goes to that
1279
- # provider's own sandbox as sent. The app is answered with a success shaped like what it sent,
1280
- # and the run is told by a dep row with `held` and the clipped call (lib/trace.cjs does the same).
1281
- import base64
1282
- from urllib.parse import parse_qsl
1283
-
1284
- passed = {"at": -1, "hosts": []}
1285
- # When the run last sent a request: a write made outside every request (a queue's worker, a
1286
- # thread) is held while a run is live, since it may be carrying out a trial's ask.
1287
- # ponytail: while a run is live, a person's own queued write is held too; per-ask pinning of
1288
- # background writes is the upgrade when that matters.
1289
- run_seen = {"at": None}
1290
- run_live_s = 600
1291
- this_machine = re.compile(r"^(?:localhost|127(?:\.\d+){3}|::1|0\.0\.0\.0)$", re.I)
1292
- # ponytail: a sign-in's token refresh is a POST that changes nothing, told apart by its path only.
1293
- token_path = re.compile(r"/(?:oauth2?/)?token(?:[/?]|$)", re.I)
1294
- # A provider's own sandbox, named so in the host: api.sandbox.paypal.com, sandbox-api.polar.sh.
1295
- sandbox_host = re.compile(r"(?:^|[.-])(?:sandbox|sbox)[.-]", re.I)
1296
- test_setting = re.compile(r"(?:^|_)TEST(?:_|$)", re.I)
1297
- # An API where every call is a POST names what a call does in its last segment, as
1298
- # resource.verb_words (customers.get, billing.preview_attach) or as a gRPC or Connect method
1299
- # (acme.v1.UserService/GetUser). A POST so named that starts with a reading word, and holds no word
1300
- # that makes the read a write (get_or_create, check_in, GetLease), goes out.
1301
- # ponytail: decided on the name alone, so a POST named as a read whose body writes goes out: a
1302
- # check that also records usage, or a file created under a name such as allow.list.
1303
- dot_call = re.compile(r"[A-Za-z][\w-]*\.[a-z][a-z_]*", re.A)
1304
- dot_read = {"get", "list", "check", "search", "query", "find", "lookup", "retrieve", "describe", "count", "preview"}
1305
- # A preview commits nothing whatever it previews; any other reading word reads only while no word
1306
- # after it writes: credits.check_deduct and usage.count_increment went out as reads.
1307
- write_word = set("create|update|delete|remove|set|add|put|insert|upsert|write|save|store|record|track|log|send|deduct|increment|decrement|consume|use|spend|charge|pay|refund|cancel|reserve|lock|unlock|commit|apply|attach|detach|start|stop|run|trigger|reset|rotate|revoke|grant|issue|mint|transfer|move|merge|sync|import|export|upload|publish|enqueue|push|pop|ack|touch|mark|close|approve|reject|submit|confirm|redeem|claim".split("|"))
1308
- dot_not = {"or", "and", "in", "out", "next"} | write_word
1309
- rpc_call = re.compile(r"(?:[A-Z][a-z0-9]*)+")
1310
- rpc_read = {"Get", "List", "Search", "Query", "Find", "Lookup", "Describe"}
1311
- rpc_not = {"or", "and", "next", "lease"} | write_word
1312
- service_name = re.compile(r"\.[A-Z]\w*\Z", re.A)
1313
-
1314
- def reads_by_name(method, path):
1315
- if str(method or "").upper() != "POST":
1316
- return False
1317
- parts = [p for p in (path or "").split("/") if p]
1318
- last = parts[-1] if parts else ""
1319
- before = parts[-2] if len(parts) > 1 else ""
1320
- if dot_call.fullmatch(last):
1321
- words = last.split(".")[1].split("_")
1322
- return words[0] in dot_read and (words[0] == "preview" or not any(w in dot_not for w in words[1:]))
1323
- if service_name.search(before) and rpc_call.fullmatch(last):
1324
- words = re.findall(r"[A-Z][a-z0-9]*", last)
1325
- return words[0] in rpc_read and not any(w.lower() in rpc_not for w in words[1:])
1326
- return False
1327
-
1328
- def pass_now():
1329
- try:
1330
- at = os.stat(_OUTBOUND).st_mtime_ns
1331
- if at != passed["at"]:
1332
- passed["at"] = at
1333
- with open(_OUTBOUND, encoding="utf-8") as f:
1334
- got = json.load(f).get("pass")
1335
- passed["hosts"] = [h for h in got if isinstance(h, str) and h] if isinstance(got, list) else []
1336
- except Exception:
1337
- pass
1338
- return passed["hosts"]
1339
-
1340
- # A provider's test-mode key, named so in the key itself whatever comes before it (Stripe's
1341
- # sk_test_, am_sk_test_, pdl_sdbx_), or a value of a setting the app keeps as a test one.
1342
- def test_key(auth):
1343
- # `requests` takes a header value as bytes, and str() of one is "b'...'", which no key matches.
1344
- a = auth.decode("utf-8", "replace") if isinstance(auth, (bytes, bytearray)) else str(auth or "")
1345
- m = re.match(r"^Basic\s+(\S+)$", a, re.I)
1346
- if m:
1347
- try:
1348
- a = base64.b64decode(m.group(1)).decode("utf-8", "replace")
1349
- except Exception:
1350
- pass
1351
- # In the prefix of the key itself, set off by underscores: a Basic user named test-admin and a
1352
- # live key holding "-Test-" are not test keys.
1353
- return bool(a) and (any(re.fullmatch(r"(?:[a-z0-9]{2,8}_){0,3}(?:test|sandbox|sbox|sdbx)_[\w-]{4,}", piece, re.I | re.A) for piece in re.split(r"[\s:]+", a)) or any(v and len(v) >= 8 and v in a for k, v in os.environ.items() if test_setting.search(k)))
1354
-
1355
- # The headers a key travels in.
1356
- key_headers = ("authorization", "x-api-key", "api-key", "apikey", "x-auth-token")
1357
-
1358
- def keys_in(headers):
1359
- return [headers.get(n) for n in key_headers]
1360
-
1361
- # `keys_of()`: the values of the headers a key travels in, read only for a call that is otherwise
1362
- # held. Once a tagged write is being decided, a failure to decide holds it.
1363
- def held_here(url, method, keys_of):
1364
- req = ctx.get()
1365
- if str(method or "").upper() not in ("POST", "PUT", "PATCH", "DELETE"):
1366
- return False
1367
- if req:
1368
- if not req.get("turn"):
1369
- return False
1370
- elif run_seen["at"] is None or time.monotonic() - run_seen["at"] > run_live_s:
1371
- return False
1372
- # A read of a Qdrant collection, however its server is named, and any call to a copy this
1373
- # hook made (copy_of below), go out.
1374
- q = qdrant_call(url, method)
1375
- if q and (not q["write"] or (q["collection"] and our_copy.search(q["collection"]))):
1376
- return False
1377
- try:
1378
- parts = urlsplit(str(url))
1379
- host = (parts.hostname or "").lower()
1380
- if this_machine.match(host) or sandbox_host.search(host) or is_model_call(url) or is_retrieval(url) or token_path.search(parts.path or "") or reads_by_name(method, parts.path) or any(test_key(k) for k in keys_of()):
1381
- return False
1382
- return not any(host == p or host.endswith("." + p) for p in pass_now())
1383
- except Exception:
1384
- return True
1385
-
1386
- # A write to a Qdrant collection made inside a run goes to a copy of that collection on the same
1387
- # server, made at the first such write: the collection is read page by page and written under
1388
- # `<name>_cortad_<pid>`, and from then on every call a run makes to that collection, reads too,
1389
- # goes to the copy, so the app finds what it wrote. The original is never written. A copy that
1390
- # cannot be made (over the size a run copies, a server that refused) leaves the write held like
1391
- # any other. Each copy is reported as a `copy` row and the command deletes them when it ends.
1392
- # The person's own calls, outside a run, go where they were sent (lib/trace.cjs does the same).
1393
- import threading
1394
- our_copy = re.compile(r"_cortad_\d+$")
1395
- copies = {}
1396
- copies_lock = threading.Lock()
1397
- qdrant_cap = 200000
1398
- # What one process copies at most: collections, and points across them; past it a write is held.
1399
- copies_max = 8
1400
- points_max = 300000
1401
- copied = {"collections": 0, "points": 0}
1402
- copy_said = set()
1403
-
1404
- def copy_say(host, collection, why):
1405
- if (host, collection) in copy_said:
1406
- return
1407
- copy_said.add((host, collection))
1408
- try:
1409
- sys.stderr.write("cortad: a request Cortad sent made this app write into the Qdrant collection %s at %s. Cortad could not copy that collection on its server (%s), so the write was kept on this machine and answered with a stand-in success.\n" % (collection, host, why))
1410
- except Exception:
1411
- pass
1412
-
1413
- def in_run():
1414
- req = ctx.get()
1415
- if req:
1416
- return bool(req.get("turn"))
1417
- return run_seen["at"] is not None and time.monotonic() - run_seen["at"] <= run_live_s
1418
-
1419
- # None to leave the call alone; hold for a write of the server itself; else the collection's
1420
- # copy when there is one, or none yet for a write, which makes it.
1421
- def guarded(url, method):
1422
- call = qdrant_call(url, method)
1423
- if not call:
1424
- return None
1425
- if not call["collection"]:
1426
- return {"hold": True} if call["write"] and in_run() else None
1427
- if our_copy.search(call["collection"]) or not in_run():
1428
- return None
1429
- key = "%s|%s" % (call["parts"].netloc, call["collection"])
1430
- copy = copies.get(key)
1431
- if copy:
1432
- return {"key": key, "call": call, "copy": copy}
1433
- if not call["write"]:
1434
- return None
1435
- if call["kind"] == "schema":
1436
- return {"hold": True}
1437
- if copied["collections"] >= copies_max or copied["points"] >= points_max:
1438
- copy_say(call["parts"].netloc, call["collection"], "this run already copied %d collections, %d points" % (copied["collections"], copied["points"]))
1439
- return {"hold": True}
1440
- return {"key": key, "call": call, "copy": None}
1441
-
1442
- # Qdrant's own API over urllib, which this file leaves unpatched: never through the app's clients.
1443
- def qdrant_http(base, key, method, path, body=None):
1444
- import urllib.error
1445
- import urllib.request
1446
- data = json.dumps(body).encode("utf-8") if body is not None else None
1447
- headers = {"content-type": "application/json"}
1448
- if key:
1449
- headers["api-key"] = key
1450
- req = urllib.request.Request(base + path, data=data, method=method, headers=headers)
1451
- try:
1452
- with urllib.request.urlopen(req, timeout=30) as r:
1453
- return json.loads(r.read().decode("utf-8", "replace") or "null"), r.status
1454
- except urllib.error.HTTPError as e:
1455
- return None, e.code
1456
-
1457
- # The copy made: "empty" when the server holds no such collection yet (the app makes it, under
1458
- # the copy's name), "points" once every point is across, None when none could be made.
1459
- def clone_qdrant(base, key, source, clone):
1460
- at = lambda name: "/collections/" + quote(name, safe="")
1461
- info, status = qdrant_http(base, key, "GET", at(source))
1462
- if status == 404:
1463
- return ("empty", 0)
1464
- result = (info or {}).get("result") if status == 200 else None
1465
- if not result:
1466
- return None
1467
- if (result.get("points_count") or 0) > qdrant_cap:
1468
- return None
1469
- params = (result.get("config") or {}).get("params") or {}
1470
- made = {"vectors": params.get("vectors")}
1471
- if params.get("sparse_vectors"):
1472
- made["sparse_vectors"] = params["sparse_vectors"]
1473
- if "on_disk_payload" in params:
1474
- made["on_disk_payload"] = params["on_disk_payload"]
1475
- if qdrant_http(base, key, "PUT", at(clone), made)[1] != 200:
1476
- return None
1477
- try:
1478
- for field, schema in (result.get("payload_schema") or {}).items():
1479
- qdrant_http(base, key, "PUT", at(clone) + "/index?wait=true", {"field_name": field, "field_schema": schema.get("params") or schema.get("data_type")})
1480
- offset = None
1481
- while True:
1482
- page, status = qdrant_http(base, key, "POST", at(source) + "/points/scroll", {"limit": 256, "with_payload": True, "with_vector": True, **({"offset": offset} if offset is not None else {})})
1483
- if status != 200:
1484
- raise RuntimeError(status)
1485
- got = (page or {}).get("result") or {}
1486
- points = got.get("points") or []
1487
- if points and qdrant_http(base, key, "PUT", at(clone) + "/points?wait=true", {"points": [{"id": p.get("id"), "vector": p.get("vector"), "payload": p.get("payload")} for p in points]})[1] != 200:
1488
- raise RuntimeError("upsert")
1489
- offset = got.get("next_page_offset")
1490
- if offset is None:
1491
- return ("points", result.get("points_count") or 0)
1492
- except Exception:
1493
- qdrant_http(base, key, "DELETE", at(clone))
1494
- return None
1495
-
1496
- # The copy's name, made now when none exists; None when none could be made. The key the app
1497
- # sent is the key the copy is made with, and never leaves this process.
1498
- def copy_of(g, header_of):
1499
- with copies_lock:
1500
- have = copies.get(g["key"])
1501
- if have:
1502
- return have
1503
- parts = g["call"]["parts"]
1504
- base = "%s://%s" % (parts.scheme, parts.netloc)
1505
- auth = str(header_of("authorization") or "")
1506
- bearer = re.match(r"^Bearer\s+(\S+)", auth, re.I)
1507
- key = str(header_of("api-key") or "") or (bearer.group(1) if bearer else "")
1508
- clone = "%s_cortad_%d" % (g["call"]["collection"][:40], os.getpid())
1509
- made = clone_qdrant(base, key, g["call"]["collection"], clone)
1510
- if not made:
1511
- copy_say(parts.netloc, g["call"]["collection"], "the copy did not go through")
1512
- return None
1513
- copied["collections"] += 1
1514
- copied["points"] += made[1]
1515
- copies[g["key"]] = clone
1516
- write({"copy": {"host": parts.hostname or "", "base": base, "collection": g["call"]["collection"], "clone": clone}})
1517
- return clone
1518
-
1519
- def to_copy(url, clone, call):
1520
- parts = call["parts"]
1521
- return urlunsplit((parts.scheme, parts.netloc, "/collections/" + quote(clone, safe="") + call["rest"], parts.query, parts.fragment))
1522
-
1523
- # The copy a call goes to, waited for off the event loop: None to hold the call instead.
1524
- async def copy_for(g, header_of):
1525
- import asyncio
1526
- if g.get("hold"):
1527
- return None
1528
- return g["copy"] or await asyncio.get_running_loop().run_in_executor(None, copy_of, g, header_of)
1529
-
1530
- # A GraphQL query reads; a mutation writes. A `query` in JSON that is not GraphQL (SQL over HTTP,
1531
- # where a SELECT can write through a function) is held.
1532
- # ClickHouse, Trino and Presto take every query as a POST of its text, a SELECT too, and a SELECT
1533
- # there cannot write: one statement that starts as a read and holds no word that writes, sent to
1534
- # what its address or its headers show is one of them, goes out (lib/trace.cjs does the same).
1535
- sql_quoted = re.compile(r"""'(?:[^'\\]|\\.|'')*'|"(?:[^"\\]|\\.|"")*"|`[^`]*`|--[^\n]*|/\*[\s\S]*?\*/""")
1536
- sql_shows = re.compile(r"^(?:show|describe|desc|exists)\b", re.I | re.A)
1537
- sql_selects = re.compile(r"^(?:select|with|explain)\b", re.I | re.A)
1538
- sql_writes = re.compile(r"(?<![.\w])(?:insert|update|delete|merge|into|create|alter|drop|truncate|refresh|call|execute)(?![.\w])", re.I | re.A)
1539
- sql_hands_on = re.compile(r"\b(?:table|query|jdbc|odbc|executable)\s*\(", re.I | re.A)
1540
- sql_open = re.compile(r"""[;'"`]|/\*|\$\$""")
1541
- analytics_name = re.compile(r"clickhouse|trino|presto", re.I)
1542
- analytics_headers = ("x-clickhouse-user", "x-clickhouse-key", "x-clickhouse-database", "x-clickhouse-format", "x-trino-user", "x-presto-user")
1543
-
1544
- def statement_of(sql):
1545
- bare = sql_quoted.sub(lambda m: "''" if m.group(0)[0] == "'" else "x" if m.group(0)[0] in '"`' else " ", str(sql or "")).strip()
1546
- return re.sub(r";\s*$", "", bare)
1547
-
1548
- # A quote or a comment left open, a second statement or a dollar-quoted body is not read as a read.
1549
- def selects(bare):
1550
- if sql_open.search(bare.replace("''", "")):
1551
- return False
1552
- return bool(sql_shows.match(bare)) or bool(sql_selects.match(bare) and not sql_writes.search(bare) and not sql_hands_on.search(bare))
1553
-
1554
- def analytics(parts, header_of):
1555
- try:
1556
- port = parts.port
1557
- except ValueError:
1558
- port = None
1559
- return port in (8123, 8443) or bool(analytics_name.search(parts.hostname or "")) or bool(analytics_name.search(str(header_of("user-agent") or ""))) \
1560
- or any(header_of(name) for name in analytics_headers) or parts.path == "/v1/statement"
1561
-
1562
- def reads_only(raw, url=None, header_of=None):
1563
- sent = str(raw or "").strip()
1564
- b = parsed(sent) if sent else None
1565
- if b is not None:
1566
- q = b.get("query") if isinstance(b, dict) else None
1567
- return isinstance(q, str) and bool(re.match(r"^\s*(?:\{|query\b|fragment\b)", q)) and not re.search(r"\bmutation\b", q)
1568
- try:
1569
- parts = urlsplit(str(url)) if url else None
1570
- if parts is None or not analytics(parts, header_of or (lambda name: None)):
1571
- return False
1572
- # ClickHouse joins a `query` in the address to the body: both are read as the one statement.
1573
- asked = "%s %s" % (dict(parse_qsl(parts.query)).get("query", ""), sent)
1574
- # ClickHouse reads a backslash before a quote as part of the string and Trino as its end,
1575
- # so where a string stops is not known here: held.
1576
- return not re.search(r"""\\['"`]""", asked) and selects(statement_of(asked))
1577
- except Exception:
1578
- return False
1579
-
1580
- held_count = itertools.count(1)
1581
-
1582
- def fields_of(raw, kind):
1583
- try:
1584
- b = dict(parse_qsl(raw)) if "form" in (kind or "") else json.loads(raw)
1585
- except Exception:
1586
- return {}
1587
- return b if isinstance(b, dict) else {}
1588
-
1589
- def held_reply(raw, kind):
1590
- return json.dumps({**fields_of(raw, kind), "id": "cortad-held-%d" % next(held_count), "status": "ok"}).encode("utf-8")
1591
-
1592
- # Said once per method and host in the app's own output, where the error that follows is read:
1593
- # a stand-in an SDK cannot parse otherwise reads as a bug in the app. It holds no word that marks
1594
- # an error, so it is never quoted back as the app's own.
1595
- held_said = set()
1596
-
1597
- def held_say(method, host):
1598
- if (method, host) in held_said:
1599
- return
1600
- held_said.add((method, host))
1601
- try:
1602
- sys.stderr.write("cortad: a request Cortad sent made this app send %s to %s. Cortad kept that call on this machine and answered it with a stand-in success, since it could change something real. A failure right after this line may come from that stand-in rather than from your code. With that service's test key the call goes out as sent.\n" % (method, host))
1603
- except Exception:
1604
- pass
1605
-
1606
- # Named by method and host only: a webhook's path is its credential.
1607
- def held_row(url, method, raw, kind, req):
1608
- try:
1609
- parts = urlsplit(str(url))
1610
- held_say(str(method).upper(), (parts.hostname or "")[:253])
1611
- write({"dep": {"at": int(time.time() * 1000), "host": (parts.hostname or "")[:253], "status": 200, "held": True, **inside_of(req),
1612
- "called": [{"name": ("%s %s" % (str(method).upper(), parts.hostname))[:80], "arguments": args_text(fields_of(raw, kind))}]}})
1613
- except Exception:
1614
- pass
1615
-
1616
- # aiohttp builds its own response from a connection, so a held call is sent to a server of this
1617
- # hook's own on this machine, which answers it.
1618
- held_server = {"port": None}
1619
- held_waiting = {}
1620
- held_sent = itertools.count(1)
1621
-
1622
- def held_port():
1623
- if held_server["port"] is None:
1624
- try:
1625
- import threading
1626
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
1627
-
1628
- class Answer(BaseHTTPRequestHandler):
1629
- def answer(self):
1630
- size = int(self.headers.get("content-length") or 0)
1631
- raw = self.rfile.read(min(size, limit)).decode("utf-8", "replace") if size else ""
1632
- kind = self.headers.get("content-type", "")
1633
- waiting = held_waiting.pop(self.headers.get("x-cortad-held", ""), None)
1634
- if waiting:
1635
- held_row(waiting[0], waiting[1], raw, kind, waiting[2])
1636
- out = held_reply(raw, kind)
1637
- self.send_response(200)
1638
- self.send_header("content-type", "application/json")
1639
- self.send_header("content-length", str(len(out)))
1640
- self.end_headers()
1641
- self.wfile.write(out)
1642
-
1643
- do_POST = do_PUT = do_PATCH = do_DELETE = answer
1644
-
1645
- def log_message(self, *a):
1646
- pass
1647
-
1648
- server = ThreadingHTTPServer(("127.0.0.1", 0), Answer)
1649
- server.daemon_threads = True
1650
- _held_ports.append(server.server_address[1])
1651
- threading.Thread(target=server.serve_forever, daemon=True).start()
1652
- held_server["port"] = server.server_address[1]
1653
- except Exception:
1654
- return None
1655
- return held_server["port"]
1656
-
1657
1272
  def said_of(req, name):
1658
1273
  return [v for k, v in req.get("reply_headers") or [] if k == name]
1659
1274
 
@@ -2181,8 +1796,6 @@ def _install():
2181
1796
  req = {"id": "%d.%d" % (os.getpid(), next(serial)), "at": int(time.time() * 1000), "method": method, "path": path, "headers": headers,
2182
1797
  "chunks": [], "size": 0, "noted": False, "kept": False, "sent": [], "reply": [], "reply_size": 0, "writes": 0,
2183
1798
  "turn": turn if isinstance(turn, str) and turn_ok.match(turn) else None}
2184
- if req["turn"]:
2185
- run_seen["at"] = time.monotonic()
2186
1799
  if tied:
2187
1800
  tied.opened(req)
2188
1801
  return req
@@ -2642,37 +2255,11 @@ def _install():
2642
2255
  dep(request.url, response.status_code, req=req, caller=caller)
2643
2256
  return response
2644
2257
 
2645
- def held_answer(request, sent):
2646
- kind = request.headers.get("content-type", "")
2647
- held_row(request.url, request.method, sent, kind, ctx.get())
2648
- return module.Response(200, headers={"content-type": "application/json"}, content=held_reply(sent, kind), request=request)
2649
-
2650
- # A held call that cannot be answered fails in the app; it is never sent instead.
2651
- def held_httpx(request, sent):
2652
- if not held_here(request.url, request.method, lambda: keys_in(request.headers)) or reads_only(sent, request.url, request.headers.get):
2653
- return None
2654
- return held_answer(request, sent)
2655
-
2656
- # A call to a Qdrant collection goes to the run's copy of it, `clone`; with none, it is held.
2657
- def routed_httpx(request, g, clone, sent):
2658
- if clone is None:
2659
- return held_answer(request, sent)
2660
- request.url = module.URL(to_copy(request.url, clone, g["call"]))
2661
- return None
2662
-
2663
2258
  for cls in (module.Client, module.AsyncClient):
2664
2259
  send = cls.send
2665
2260
  if cls is module.Client:
2666
2261
  def sync_send(self, request, *a, _send=send, **k):
2667
2262
  sent = sent_of(request)
2668
- g = guarded(request.url, request.method)
2669
- if g is not None:
2670
- answered = routed_httpx(request, g, None if g.get("hold") else (g["copy"] or copy_of(g, request.headers.get)), sent)
2671
- if answered is not None:
2672
- return answered
2673
- held = held_httpx(request, sent)
2674
- if held is not None:
2675
- return held
2676
2263
  caller = callers_now(request.url) if is_model_call(request.url) or is_retrieval(request.url) else None
2677
2264
  t0 = time.time()
2678
2265
  req = noted(request.url, sent)
@@ -2693,14 +2280,6 @@ def _install():
2693
2280
  else:
2694
2281
  async def async_send(self, request, *a, _send=send, **k):
2695
2282
  sent = sent_of(request)
2696
- g = guarded(request.url, request.method)
2697
- if g is not None:
2698
- answered = routed_httpx(request, g, await copy_for(g, request.headers.get), sent)
2699
- if answered is not None:
2700
- return answered
2701
- held = held_httpx(request, sent)
2702
- if held is not None:
2703
- return held
2704
2283
  caller = callers_now(request.url) if is_model_call(request.url) or is_retrieval(request.url) else None
2705
2284
  t0 = time.time()
2706
2285
  req = noted(request.url, sent)
@@ -2722,36 +2301,7 @@ def _install():
2722
2301
  def patch_requests(module):
2723
2302
  send = module.Session.send
2724
2303
 
2725
- # A held call that cannot be answered fails in the app; it is never sent instead.
2726
- def held(request):
2727
- if not held_here(request.url, request.method, lambda: keys_in(request.headers)):
2728
- return None
2729
- raw = text(request.body, reply_max)
2730
- if reads_only(raw, request.url, request.headers.get):
2731
- return None
2732
- return held_answer(request, raw)
2733
-
2734
- def held_answer(request, raw):
2735
- import datetime
2736
- models, structures = sys.modules["requests.models"], sys.modules["requests.structures"]
2737
- kind = request.headers.get("content-type", "")
2738
- held_row(request.url, request.method, raw, kind, ctx.get())
2739
- response = models.Response()
2740
- response.status_code, response.reason, response.url, response.request, response.encoding = 200, "OK", request.url, request, "utf-8"
2741
- response.headers = structures.CaseInsensitiveDict({"content-type": "application/json"})
2742
- response._content, response._content_consumed, response.elapsed = held_reply(raw, kind), True, datetime.timedelta(0)
2743
- return response
2744
-
2745
2304
  def sent(self, request, *a, **k):
2746
- g = guarded(request.url, request.method)
2747
- if g is not None:
2748
- clone = None if g.get("hold") else (g["copy"] or copy_of(g, request.headers.get))
2749
- if clone is None:
2750
- return held_answer(request, text(request.body, reply_max))
2751
- request.url = to_copy(request.url, clone, g["call"])
2752
- answered = held(request)
2753
- if answered is not None:
2754
- return answered
2755
2305
  caller = callers_now(request.url) if is_model_call(request.url) or is_retrieval(request.url) else None
2756
2306
  t0 = time.time()
2757
2307
  req = noted(request.url, request.body)
@@ -2781,57 +2331,8 @@ def _install():
2781
2331
  def patch_aiohttp(module):
2782
2332
  request = module.ClientSession._request
2783
2333
 
2784
- def keys_of(self, k):
2785
- given = {str(n).lower(): v for n, v in dict(k.get("headers") or {}).items()}
2786
- defaults = {str(n).lower(): v for n, v in dict(getattr(self, "_default_headers", None) or {}).items()}
2787
- basic = k.get("auth") or getattr(self, "_default_auth", None)
2788
- return [given.get(n) or defaults.get(n) for n in key_headers] + [basic.encode() if basic else None]
2789
-
2790
- def header_of(self, k, name):
2791
- for headers in (k.get("headers"), getattr(self, "_default_headers", None)):
2792
- for n, v in dict(headers or {}).items():
2793
- if str(n).lower() == name:
2794
- return v
2795
- return None
2796
-
2797
- # A path relative to the session's base address is decided on the whole address.
2798
- def whole(self, str_or_url):
2799
- try:
2800
- return str(self._build_url(str_or_url))
2801
- except Exception:
2802
- return str(str_or_url)
2803
-
2804
2334
  async def requested(self, method, str_or_url, *a, **k):
2805
2335
  body = k.get("json") if k.get("json") is not None else k.get("data")
2806
- url = whole(self, str_or_url)
2807
- g = guarded(url, method)
2808
- hold = False
2809
- if g is not None:
2810
- clone = await copy_for(g, lambda name: header_of(self, k, name))
2811
- if clone is None:
2812
- hold = True
2813
- else:
2814
- parts = g["call"]["parts"]
2815
- url = to_copy(url, clone, g["call"])
2816
- # A session with a base address takes the path alone.
2817
- str_or_url = url if getattr(self, "_base_url", None) is None else url[len("%s://%s" % (parts.scheme, parts.netloc)):]
2818
- if hold or (held_here(url, method, lambda: keys_of(self, k)) and not reads_only(text(body, reply_max), url, lambda name: header_of(self, k, name))):
2819
- port = held_port()
2820
- if port is None:
2821
- raise module.ClientConnectionError("this write was kept from leaving and could not be answered")
2822
- parts = urlsplit(url)
2823
- n = str(next(held_sent))
2824
- held_waiting[n] = (url, method, ctx.get())
2825
- k = {**k, "headers": {**dict(k.get("headers") or {}), "x-cortad-held": n}}
2826
- target = "http://127.0.0.1:%d%s%s" % (port, parts.path or "/", "?" + parts.query if parts.query else "")
2827
- if getattr(self, "_base_url", None) is None:
2828
- return await request(self, method, target, *a, **k)
2829
- # A session with a base address refuses a whole one on older aiohttp: the held call is
2830
- # answered through a session of its own, read before that session closes.
2831
- async with module.ClientSession() as own:
2832
- answered = await request(own, method, target, *a, **k)
2833
- await answered.read()
2834
- return answered
2835
2336
  caller = callers_now(str_or_url) if is_model_call(str_or_url) or is_retrieval(str_or_url) else None
2836
2337
  t0 = time.time()
2837
2338
  req = noted(str_or_url, body)
@@ -2974,7 +2475,7 @@ def _install_connects():
2974
2475
  if not isinstance(address, tuple) or len(address) < 2 or len(seen) >= 64:
2975
2476
  return
2976
2477
  host, port = str(address[0]).lower()[:253], int(address[1])
2977
- if (host, port) in seen or port in _held_ports:
2478
+ if (host, port) in seen:
2978
2479
  return
2979
2480
  seen.add((host, port))
2980
2481
  fd = os.open(_FILE, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)