User1342 commited on
Commit
3c241d8
·
1 Parent(s): fabf319

Workers print their own code and are claimed from the site; fetch and verify llama.cpp

Browse files
distinct_agent/cli.py CHANGED
@@ -152,16 +152,42 @@ def _runner_problem(args) -> str:
152
  )
153
  if _find_llama_server():
154
  return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  return (
156
- "This worker has no llama.cpp to run a model with.\n"
 
157
  "\n"
158
- " llama.cpp is a separate program, not part of this project. Get a build\n"
159
- " from https://github.com/ggml-org/llama.cpp/releases, unzip it, and either\n"
160
- " put llama-server on your PATH or point at it:\n"
161
  "\n"
162
- " --llama-server C:\\path\\to\\llama-server.exe\n"
163
  "\n"
164
- " Or start without a model. The worker still pairs and still runs every\n"
165
  " tool and skill in the library; only the model's own replies are absent:\n"
166
  "\n"
167
  " --demo-runner\n"
@@ -169,11 +195,18 @@ def _runner_problem(args) -> str:
169
 
170
 
171
  def _find_llama_server() -> str:
172
- """Look in runtime/ first, then PATH."""
 
 
 
 
 
 
 
173
 
174
- bundled = Path(__file__).resolve().parent.parent / "runtime" / "llama-server.exe"
175
- if bundled.is_file():
176
- return str(bundled)
177
  from shutil import which
178
 
179
  for name in ("llama-server", "llama-server.exe"):
@@ -183,6 +216,49 @@ def _find_llama_server() -> str:
183
  return ""
184
 
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  def build_parser() -> argparse.ArgumentParser:
187
  parser = argparse.ArgumentParser(description="Run a Distinct local inference worker")
188
  parser.add_argument(
@@ -357,6 +433,15 @@ def build_parser() -> argparse.ArgumentParser:
357
  metavar="ID=PATH",
358
  help=argparse.SUPPRESS,
359
  )
 
 
 
 
 
 
 
 
 
360
  parser.add_argument(
361
  "--llama-server",
362
  nargs="?",
@@ -539,18 +624,34 @@ def _forget_servers(args) -> int:
539
  def _server_pairs(urls, codes):
540
  """Match each --server to its --pair, or explain the mismatch.
541
 
542
- Positional matching, because it is the only scheme that reads correctly
543
- aloud: "this server, this code; that server, that code". A mismatch is an
544
- error rather than a best guess, since guessing would mean sending one
545
- server's pairing code to another, which is a credential leak dressed up as
546
- convenience.
 
 
 
 
 
 
 
 
547
  """
548
 
 
 
 
 
 
 
 
549
  if len(codes) != len(urls):
550
  print(
551
  f"{len(urls)} --server value(s) and {len(codes)} --pair value(s). "
552
- "Each server needs its own fresh pairing code, given in the same "
553
- "order. Codes are not interchangeable between servers.",
 
554
  file=sys.stderr,
555
  )
556
  return None
@@ -578,6 +679,92 @@ def _servers_file(explicit: str | None) -> Path:
578
  return shared_cache_directory().parent / "servers.json"
579
 
580
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
  def _join_server(
582
  url,
583
  code,
@@ -618,24 +805,38 @@ def _join_server(
618
  tools=capabilities.tools,
619
  )
620
  identity_key = generate_ed25519_key()
621
- try:
622
- transport = GradioAgentTransport.pair(
 
623
  url,
624
- code,
625
  capabilities,
626
  access_code=access_code,
627
  client_factory=client_factory,
628
- allow_remote=allow_remote or None,
629
- allow_plaintext=allow_plaintext or None,
630
  signing_public_key=ed25519_public_key(identity_key),
631
  )
632
- except (AgentTransportError, TypeError, ValueError) as exc:
633
- print(f"Pairing with {url} failed: {exc}", file=sys.stderr)
634
- return None, None
635
- print(
636
- f"Paired {capabilities.name!r} with {url} as {transport.agent_id}.",
637
- file=sys.stderr,
638
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
  # -------- Consent handshake: catalogue, approval, gate ----------
641
  #
@@ -786,9 +987,6 @@ def main(
786
  # crash. Argparse-level mistakes deserve argparse-level answers.
787
  print("--demo-prompt needs something to ask; it was empty.", file=sys.stderr)
788
  return 2
789
- if args.server and transport is None and not args.pair:
790
- print("--server requires a fresh --pair code.", file=sys.stderr)
791
- return 2
792
  if args.list_servers:
793
  return _list_servers(args)
794
  if args.forget_server:
@@ -1290,15 +1488,17 @@ def main(
1290
  # time anybody sees it: the servers keep a digest and this process
1291
  # does not write it anywhere.
1292
  access_code = new_access_code()
1293
- # `--server` is repeatable, so this is a list. One worker can serve
1294
- # several servers with the same code, and the invitation names them
1295
- # all rather than picking one and quietly misdirecting the people
1296
- # sent to the others.
1297
- print(
1298
- announce_access_code(access_code, servers=tuple(args.server or ())),
1299
- file=sys.stderr,
1300
- flush=True,
1301
- )
 
 
1302
  # Kept so the dashboard can show it. It is printed once and stored
1303
  # nowhere on disk, so a screen that could not show it would leave
1304
  # somebody who scrolled past it with no way to get it back short of
@@ -1338,6 +1538,15 @@ def main(
1338
  flush=True,
1339
  )
1340
  return 2
 
 
 
 
 
 
 
 
 
1341
  if len(links) == 1:
1342
  # One server: the transport it paired with, unwrapped. The
1343
  # fan-out is a real object with real behaviour and there is no
 
152
  )
153
  if _find_llama_server():
154
  return ""
155
+ if _obtain_llama_server(args):
156
+ return ""
157
+ from . import runtime as _runtime
158
+
159
+ where = _runtime.platform_key()
160
+ if _runtime.pin_for(where) is None:
161
+ # Nothing recorded for this platform, so nothing was attempted. Say
162
+ # which platform, because the answer is "add a pin for it" and that is
163
+ # a thing somebody can do.
164
+ return (
165
+ "This worker has no llama.cpp to run a model with, and none is recorded\n"
166
+ f"for {where}, so nothing was downloaded.\n"
167
+ "\n"
168
+ " An unverified build of the program that runs the models is the one\n"
169
+ " thing this project will not install for you. Either point at a build\n"
170
+ " you already have:\n"
171
+ "\n"
172
+ " --llama-server /path/to/llama-server\n"
173
+ "\n"
174
+ " or start without a model. The worker still joins, and still runs every\n"
175
+ " tool and skill in the library; only the model's own replies are absent:\n"
176
+ "\n"
177
+ " --demo-runner\n"
178
+ "\n"
179
+ f" To record a build for {where}, run:\n"
180
+ f" python scripts/record_llama_pin.py {where}\n"
181
+ )
182
  return (
183
+ "This worker has no llama.cpp to run a model with, and the pinned build\n"
184
+ "could not be installed. The reason is above.\n"
185
  "\n"
186
+ " Point at a build you already have:\n"
 
 
187
  "\n"
188
+ " --llama-server /path/to/llama-server\n"
189
  "\n"
190
+ " or start without a model. The worker still joins, and still runs every\n"
191
  " tool and skill in the library; only the model's own replies are absent:\n"
192
  "\n"
193
  " --demo-runner\n"
 
195
 
196
 
197
  def _find_llama_server() -> str:
198
+ """Look in runtime/ first, then PATH.
199
+
200
+ ``runtime/`` is where :func:`distinct_agent.runtime.ensure` installs the
201
+ pinned build, so a worker that fetched one on a previous run finds it here
202
+ and does not fetch again.
203
+ """
204
+
205
+ from . import runtime as _runtime
206
 
207
+ installed = _runtime.installed_server()
208
+ if installed:
209
+ return installed
210
  from shutil import which
211
 
212
  for name in ("llama-server", "llama-server.exe"):
 
216
  return ""
217
 
218
 
219
+ def _obtain_llama_server(args) -> str:
220
+ """Fetch and verify the pinned llama.cpp for this machine, or say why not.
221
+
222
+ Returns the path, or an empty string when nothing could be installed. The
223
+ reasons are printed as they happen rather than collected, because the
224
+ download is the slow part and a volunteer watching a progress line wants to
225
+ know what it is for.
226
+ """
227
+
228
+ from . import runtime as _runtime
229
+
230
+ if getattr(args, "no_fetch_runtime", False):
231
+ return ""
232
+ pin = _runtime.pin_for()
233
+ if pin is None:
234
+ return ""
235
+ print(
236
+ f"No llama.cpp on this machine. Fetching the pinned build "
237
+ f"({pin.tag}, {pin.bytes / (1024 * 1024):.0f} MB) and verifying it against "
238
+ "its recorded SHA-256. This happens once.",
239
+ file=sys.stderr,
240
+ flush=True,
241
+ )
242
+ last = [-1]
243
+
244
+ def report(fraction: float, message: str) -> None:
245
+ percent = int(fraction * 100)
246
+ if percent // 10 != last[0] // 10 or fraction >= 1.0:
247
+ last[0] = percent
248
+ print(f" llama.cpp: {message} ({percent}%)", file=sys.stderr, flush=True)
249
+
250
+ try:
251
+ return _runtime.ensure(progress=report)
252
+ except _runtime.RuntimeVerificationError as exc:
253
+ # A digest mismatch is not a transient failure and must not be retried
254
+ # around: it is what a substituted build looks like.
255
+ print(f"\n{exc}\n", file=sys.stderr, flush=True)
256
+ return ""
257
+ except _runtime.RuntimeUnavailable as exc:
258
+ print(f" llama.cpp could not be installed: {exc}", file=sys.stderr, flush=True)
259
+ return ""
260
+
261
+
262
  def build_parser() -> argparse.ArgumentParser:
263
  parser = argparse.ArgumentParser(description="Run a Distinct local inference worker")
264
  parser.add_argument(
 
433
  metavar="ID=PATH",
434
  help=argparse.SUPPRESS,
435
  )
436
+ parser.add_argument(
437
+ "--no-fetch-runtime",
438
+ action="store_true",
439
+ help=(
440
+ "never download llama.cpp. The worker uses one already on this "
441
+ "machine or refuses to run models, which is what an operator on a "
442
+ "metered or air-gapped connection wants."
443
+ ),
444
+ )
445
  parser.add_argument(
446
  "--llama-server",
447
  nargs="?",
 
624
  def _server_pairs(urls, codes):
625
  """Match each --server to its --pair, or explain the mismatch.
626
 
627
+ With no ``--pair`` values at all, every server is paired the other way
628
+ round: the worker invents a claim code, prints it, and waits for somebody
629
+ signed in to enter it. That is the path the website documents, because the
630
+ old one -- fetch a code from the page, carry it to a terminal -- reliably
631
+ produced a person typing a value of their own where the code went and being
632
+ told one program later that their code was invalid.
633
+
634
+ ``--pair`` is kept for scripted setups, where a code is minted and consumed
635
+ by the same program and nobody reads anything. Positional matching, because
636
+ it is the only scheme that reads correctly aloud: "this server, this code;
637
+ that server, that code". A mismatch is an error rather than a best guess,
638
+ since guessing would mean sending one server's code to another, which is a
639
+ credential leak dressed up as convenience.
640
  """
641
 
642
+ if not codes:
643
+ # NO --pair AT ALL IS THE ORDINARY CASE NOW.
644
+ #
645
+ # Every server gets its own freshly invented claim code, and the
646
+ # operator types it into that server's page. `None` for a code is the
647
+ # signal further down to register-and-wait rather than pair.
648
+ return [(url, None) for url in urls]
649
  if len(codes) != len(urls):
650
  print(
651
  f"{len(urls)} --server value(s) and {len(codes)} --pair value(s). "
652
+ "Either give no --pair at all, and each server will print a code for "
653
+ "you to enter on its page, or give one code per server in the same "
654
+ "order. Codes are never interchangeable between servers.",
655
  file=sys.stderr,
656
  )
657
  return None
 
679
  return shared_cache_directory().parent / "servers.json"
680
 
681
 
682
+ def _register_and_wait(
683
+ url,
684
+ capabilities,
685
+ *,
686
+ access_code,
687
+ client_factory,
688
+ allow_remote,
689
+ allow_plaintext,
690
+ signing_public_key,
691
+ poll_seconds: float = 3.0,
692
+ announce=None,
693
+ sleep=None,
694
+ ):
695
+ """Print a claim code for this server and wait until somebody enters it.
696
+
697
+ Returns the transport once the worker has been claimed, or ``None`` if it
698
+ was not claimed in time or the server refused the registration.
699
+
700
+ The wait is deliberately visible. A worker that registered silently and sat
701
+ there would look identical to one that had hung, and the whole point of
702
+ this direction is that the person is looking at the terminal when the code
703
+ appears.
704
+ """
705
+
706
+ import time as _time
707
+
708
+ from .access import new_access_code as _new_code
709
+
710
+ say = announce or (lambda text: print(text, file=sys.stderr, flush=True))
711
+ nap = sleep or _time.sleep
712
+ claim_code = _new_code()
713
+ try:
714
+ transport = GradioAgentTransport.register(
715
+ url,
716
+ claim_code,
717
+ capabilities,
718
+ access_code=access_code,
719
+ client_factory=client_factory,
720
+ allow_remote=allow_remote or None,
721
+ allow_plaintext=allow_plaintext or None,
722
+ signing_public_key=signing_public_key,
723
+ )
724
+ except (AgentTransportError, TypeError, ValueError) as exc:
725
+ say(f"Registering with {url} failed: {exc}")
726
+ return None
727
+
728
+ say("")
729
+ say(" ┌─────────────────────────────────────────────────────────┐")
730
+ say(" │ This worker is waiting to be claimed. │")
731
+ say(" └─────────────────────────────────────────────────────────┘")
732
+ say("")
733
+ say(f" Sign in at {url}")
734
+ say(' open "Run a community agent", and enter this code:')
735
+ say("")
736
+ say(f" {claim_code}")
737
+ say("")
738
+ say(" Nobody can use this machine until you do, and the code works")
739
+ say(" only once. Waiting…")
740
+ say("")
741
+
742
+ deadline = _time.monotonic() + 15 * 60
743
+ while _time.monotonic() < deadline:
744
+ try:
745
+ state = transport.claim_state()
746
+ except (AgentTransportError, TypeError, ValueError):
747
+ # A poll that fails is not a claim that failed. Keep waiting; a
748
+ # server that is really gone will still be gone at the deadline.
749
+ state = "waiting"
750
+ if state == "claimed":
751
+ say(f"Claimed. {capabilities.name!r} is now serving {url} as {transport.agent_id}.")
752
+ return transport
753
+ if state == "gone":
754
+ say(
755
+ "This worker's registration expired before anybody claimed it. "
756
+ "Nothing was left running on the server; start the worker again "
757
+ "for a fresh code."
758
+ )
759
+ return None
760
+ nap(poll_seconds)
761
+ say(
762
+ "Nobody claimed this worker within fifteen minutes, so it has stopped "
763
+ "waiting. Start it again for a fresh code."
764
+ )
765
+ return None
766
+
767
+
768
  def _join_server(
769
  url,
770
  code,
 
805
  tools=capabilities.tools,
806
  )
807
  identity_key = generate_ed25519_key()
808
+ if code is None:
809
+ # THE CODE GOES FROM HERE TO THE BROWSER, NOT THE OTHER WAY.
810
+ transport = _register_and_wait(
811
  url,
 
812
  capabilities,
813
  access_code=access_code,
814
  client_factory=client_factory,
815
+ allow_remote=allow_remote,
816
+ allow_plaintext=allow_plaintext,
817
  signing_public_key=ed25519_public_key(identity_key),
818
  )
819
+ if transport is None:
820
+ return None, None
821
+ else:
822
+ try:
823
+ transport = GradioAgentTransport.pair(
824
+ url,
825
+ code,
826
+ capabilities,
827
+ access_code=access_code,
828
+ client_factory=client_factory,
829
+ allow_remote=allow_remote or None,
830
+ allow_plaintext=allow_plaintext or None,
831
+ signing_public_key=ed25519_public_key(identity_key),
832
+ )
833
+ except (AgentTransportError, TypeError, ValueError) as exc:
834
+ print(f"Pairing with {url} failed: {exc}", file=sys.stderr)
835
+ return None, None
836
+ print(
837
+ f"Paired {capabilities.name!r} with {url} as {transport.agent_id}.",
838
+ file=sys.stderr,
839
+ )
840
 
841
  # -------- Consent handshake: catalogue, approval, gate ----------
842
  #
 
987
  # crash. Argparse-level mistakes deserve argparse-level answers.
988
  print("--demo-prompt needs something to ask; it was empty.", file=sys.stderr)
989
  return 2
 
 
 
990
  if args.list_servers:
991
  return _list_servers(args)
992
  if args.forget_server:
 
1488
  # time anybody sees it: the servers keep a digest and this process
1489
  # does not write it anywhere.
1490
  access_code = new_access_code()
1491
+ # PRINTED AFTER THE JOIN, NOT BEFORE IT.
1492
+ #
1493
+ # This used to be announced here, which put two high-entropy codes
1494
+ # on screen at once: the access code to share, and then the claim
1495
+ # code to enter. Two codes visible together is the same confusion
1496
+ # this whole change exists to remove -- the person scrolls back,
1497
+ # finds *a* code, and uses it for the wrong thing.
1498
+ #
1499
+ # One at a time, in the order they are needed: the claim code while
1500
+ # the worker waits, and the access code once it is actually running
1501
+ # and there is something worth sharing.
1502
  # Kept so the dashboard can show it. It is printed once and stored
1503
  # nowhere on disk, so a screen that could not show it would leave
1504
  # somebody who scrolled past it with no way to get it back short of
 
1538
  flush=True,
1539
  )
1540
  return 2
1541
+ # `--server` is repeatable, so this is a list. One worker can serve
1542
+ # several servers with the same code, and the invitation names them
1543
+ # all rather than picking one and quietly misdirecting the people
1544
+ # sent to the others.
1545
+ print(
1546
+ announce_access_code(access_code, servers=tuple(args.server or ())),
1547
+ file=sys.stderr,
1548
+ flush=True,
1549
+ )
1550
  if len(links) == 1:
1551
  # One server: the transport it paired with, unwrapped. The
1552
  # fan-out is a real object with real behaviour and there is no
distinct_agent/runtime.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Getting llama.cpp onto a volunteer's machine, with the same rules as weights.
2
+
3
+ WHY THIS EXISTS.
4
+
5
+ The setup used to end with a wall. A volunteer cloned the repository, ran the
6
+ install, ran the worker, and was told to go to another project's releases page,
7
+ pick the right build for their machine out of a dozen, unzip it somewhere, and
8
+ either edit their PATH or pass a path back on the command line. Their reaction
9
+ was the correct one: *why do I have to provide this, it should be baked into
10
+ the repo*.
11
+
12
+ It cannot literally be in the repository. A llama.cpp release is around a
13
+ hundred megabytes per platform, there is one per operating system and per
14
+ accelerator, and it is a separate project with its own release cadence. Putting
15
+ those in a Space repository would make every clone enormous and every llama.cpp
16
+ release a commit here.
17
+
18
+ ``llama-cpp-python`` was the other obvious answer and is worse: with no
19
+ matching wheel it compiles from source, so on a Windows machine without a
20
+ build toolchain the failure moves from "run the worker" to ``pip install``
21
+ itself, which is a wall one step earlier and harder to read.
22
+
23
+ So this does what the project already does for model weights: fetches a
24
+ **pinned** release over TLS and refuses to install it unless the bytes match a
25
+ **recorded SHA-256**. One command, nothing to go and get, and the thing that
26
+ runs the models is verified rather than trusted.
27
+
28
+ WHAT IS DELIBERATELY NOT DONE.
29
+
30
+ There is no trust-on-first-use path. Reading a digest out of the same response
31
+ that carried the download proves only that the bytes arrived intact, which TLS
32
+ already said. This project withholds a *model* for exactly that reason --
33
+ ``llama-2-7b-chat`` is refused with "no upstream repository, pinned revision or
34
+ SHA-256 digest recorded, so this worker cannot obtain the exact published file
35
+ or confirm what it ran" -- and the program that executes those models cannot be
36
+ held to a lower standard than the models.
37
+
38
+ A platform with no recorded digest therefore gets no automatic download. It is
39
+ told so plainly, and keeps both manual routes: ``--llama-server`` for a build
40
+ the operator already has, and ``--demo-runner`` to run the library's tools and
41
+ skills with no model at all.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import hashlib
47
+ import json
48
+ import platform
49
+ import shutil
50
+ import stat
51
+ import sys
52
+ import tempfile
53
+ import urllib.error
54
+ import urllib.parse
55
+ import urllib.request
56
+ import zipfile
57
+ from dataclasses import dataclass
58
+ from pathlib import Path
59
+ from typing import Any, Callable, Mapping, Optional
60
+
61
+ #: Where the pins live. A data file rather than a literal so that recording a
62
+ #: new platform, or moving to a newer llama.cpp, is a data change somebody can
63
+ #: review as a diff of digests.
64
+ PIN_FILE = Path(__file__).resolve().parent.parent / "llama_runtime.json"
65
+
66
+ #: Releases are served from github.com and redirected to their object store.
67
+ #: Anywhere else and the download is refused, for the same reason the weights
68
+ #: fetcher refuses a redirect off Hugging Face: a download that has been bounced
69
+ #: somewhere unexpected is no longer the download that was asked for.
70
+ RUNTIME_REDIRECT_HOSTS = ("github.com", "githubusercontent.com", "github-releases.githubusercontent.com")
71
+
72
+ _CHUNK = 1024 * 1024
73
+
74
+ #: A llama.cpp Windows archive is about 30 MB; a CUDA one rather more. This is
75
+ #: a bomb guard, not a size expectation: an archive claiming to be larger than
76
+ #: this is refused before a byte of it is written.
77
+ MAX_ARCHIVE_BYTES = 600 * 1024 * 1024
78
+
79
+ #: The unpacked tree is checked against the same ceiling, so an archive that is
80
+ #: small on the wire and enormous on disk is refused rather than unpacked.
81
+ MAX_UNPACKED_BYTES = 900 * 1024 * 1024
82
+
83
+
84
+ class RuntimeUnavailable(RuntimeError):
85
+ """The runtime could not be obtained, with a sentence saying why."""
86
+
87
+
88
+ class RuntimeVerificationError(RuntimeUnavailable):
89
+ """The bytes arrived but did not match the recorded digest."""
90
+
91
+
92
+ class _RuntimeRedirects(urllib.request.HTTPRedirectHandler):
93
+ max_redirections = 10
94
+
95
+ @staticmethod
96
+ def _permitted(url: str) -> bool:
97
+ parsed = urllib.parse.urlsplit(url)
98
+ if parsed.scheme != "https":
99
+ return False
100
+ host = (parsed.hostname or "").casefold().rstrip(".")
101
+ if not host:
102
+ return False
103
+ return any(
104
+ host == allowed or host.endswith("." + allowed)
105
+ for allowed in RUNTIME_REDIRECT_HOSTS
106
+ )
107
+
108
+ def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D102
109
+ if not self._permitted(newurl):
110
+ raise urllib.error.HTTPError(
111
+ newurl,
112
+ code,
113
+ "the llama.cpp download was redirected off GitHub and was refused",
114
+ headers,
115
+ fp,
116
+ )
117
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
118
+
119
+
120
+ def _opener() -> Callable[..., Any]:
121
+ return urllib.request.build_opener(_RuntimeRedirects).open
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class RuntimePin:
126
+ """One platform's recorded build."""
127
+
128
+ key: str
129
+ tag: str
130
+ asset: str
131
+ url: str
132
+ sha256: str
133
+ bytes: int = 0
134
+
135
+ @property
136
+ def recorded(self) -> bool:
137
+ return bool(self.url and len(self.sha256) == 64)
138
+
139
+
140
+ def platform_key(system: str = "", machine: str = "") -> str:
141
+ """The key this machine looks itself up under.
142
+
143
+ Coarse on purpose: operating system and architecture, nothing about the
144
+ accelerator. An operator who wants a CUDA or Metal build passes
145
+ ``--llama-server``; automatic installation picks the build that runs
146
+ everywhere rather than the fastest one, because a fast build that does not
147
+ start is worse than a slow one that does.
148
+ """
149
+
150
+ system = (system or platform.system()).casefold()
151
+ machine = (machine or platform.machine()).casefold()
152
+ if machine in {"amd64", "x86_64", "x64"}:
153
+ arch = "x64"
154
+ elif machine in {"arm64", "aarch64"}:
155
+ arch = "arm64"
156
+ else:
157
+ arch = machine or "unknown"
158
+ if system.startswith("win"):
159
+ return f"windows-{arch}"
160
+ if system == "darwin":
161
+ return f"macos-{arch}"
162
+ if system == "linux":
163
+ return f"linux-{arch}"
164
+ return f"{system or 'unknown'}-{arch}"
165
+
166
+
167
+ def load_pins(path: Optional[Path] = None) -> dict[str, RuntimePin]:
168
+ """Read the recorded builds, or an empty mapping if none are recorded."""
169
+
170
+ source = path or PIN_FILE
171
+ try:
172
+ raw = json.loads(source.read_text(encoding="utf-8"))
173
+ except (OSError, ValueError):
174
+ return {}
175
+ builds = raw.get("builds") if isinstance(raw, Mapping) else None
176
+ if not isinstance(builds, Mapping):
177
+ return {}
178
+ pins: dict[str, RuntimePin] = {}
179
+ for key, entry in builds.items():
180
+ if not isinstance(entry, Mapping):
181
+ continue
182
+ pins[str(key)] = RuntimePin(
183
+ key=str(key),
184
+ tag=str(entry.get("tag", "")),
185
+ asset=str(entry.get("asset", "")),
186
+ url=str(entry.get("url", "")),
187
+ sha256=str(entry.get("sha256", "")).casefold(),
188
+ bytes=int(entry.get("bytes", 0) or 0),
189
+ )
190
+ return pins
191
+
192
+
193
+ def pin_for(key: str = "", path: Optional[Path] = None) -> Optional[RuntimePin]:
194
+ pin = load_pins(path).get(key or platform_key())
195
+ return pin if pin is not None and pin.recorded else None
196
+
197
+
198
+ def runtime_directory(root: Optional[Path] = None) -> Path:
199
+ """Where an installed llama.cpp lives: ``runtime/`` beside the project."""
200
+
201
+ return (root or PIN_FILE.parent) / "runtime"
202
+
203
+
204
+ def installed_server(root: Optional[Path] = None) -> str:
205
+ """The path to an already-installed llama-server, or empty."""
206
+
207
+ directory = runtime_directory(root)
208
+ for name in ("llama-server.exe", "llama-server"):
209
+ candidate = directory / name
210
+ if candidate.is_file():
211
+ return str(candidate)
212
+ # Some archives nest everything one level down under build/bin.
213
+ for candidate in sorted(directory.glob("**/llama-server*")):
214
+ if candidate.is_file() and candidate.suffix in {"", ".exe"}:
215
+ return str(candidate)
216
+ return ""
217
+
218
+
219
+ def _safe_members(archive: zipfile.ZipFile) -> list[zipfile.ZipInfo]:
220
+ """Every member that may be written, refusing traversal and bombs.
221
+
222
+ ``ZipFile.extractall`` sanitises paths in modern Python, but it has no
223
+ opinion about total size, and this archive comes from outside the project.
224
+ Both are checked here so the refusal names the member rather than filling a
225
+ volunteer's disk and failing later.
226
+ """
227
+
228
+ total = 0
229
+ members: list[zipfile.ZipInfo] = []
230
+ for info in archive.infolist():
231
+ name = info.filename
232
+ if name.endswith("/"):
233
+ continue
234
+ pure = Path(name)
235
+ if pure.is_absolute() or ".." in pure.parts or (len(name) > 1 and name[1] == ":"):
236
+ raise RuntimeUnavailable(
237
+ f"the llama.cpp archive contains an unsafe path ({name!r}) and was not unpacked"
238
+ )
239
+ total += info.file_size
240
+ if info.file_size > MAX_UNPACKED_BYTES or total > MAX_UNPACKED_BYTES:
241
+ raise RuntimeUnavailable(
242
+ "the llama.cpp archive unpacks to more than "
243
+ f"{MAX_UNPACKED_BYTES // (1024 * 1024)} MB and was refused"
244
+ )
245
+ members.append(info)
246
+ return members
247
+
248
+
249
+ def ensure(
250
+ *,
251
+ root: Optional[Path] = None,
252
+ key: str = "",
253
+ progress: Optional[Callable[[float, str], None]] = None,
254
+ opener: Optional[Callable[..., Any]] = None,
255
+ pins_path: Optional[Path] = None,
256
+ ) -> str:
257
+ """Install the pinned llama.cpp for this machine and return its path.
258
+
259
+ Returns the path to ``llama-server``. Raises :class:`RuntimeUnavailable`
260
+ with a sentence a volunteer can act on when there is nothing recorded for
261
+ this platform, and :class:`RuntimeVerificationError` when the bytes do not
262
+ match -- in which case nothing is installed.
263
+ """
264
+
265
+ existing = installed_server(root)
266
+ if existing:
267
+ return existing
268
+
269
+ resolved = key or platform_key()
270
+ pin = pin_for(resolved, pins_path)
271
+ if pin is None:
272
+ raise RuntimeUnavailable(
273
+ f"no llama.cpp build is recorded for {resolved}, so this worker will not "
274
+ "download one: an unverified build of the program that runs the models is "
275
+ "exactly what this project refuses to install. Point at a build you already "
276
+ "have with --llama-server, or start without a model using --demo-runner."
277
+ )
278
+
279
+ target = runtime_directory(root)
280
+ target.mkdir(parents=True, exist_ok=True)
281
+ fetch = opener or _opener()
282
+ digest = hashlib.sha256()
283
+ written = 0
284
+
285
+ if progress is not None:
286
+ progress(0.0, f"Fetching llama.cpp {pin.tag} for {resolved}")
287
+
288
+ with tempfile.TemporaryDirectory(prefix="distinct-runtime-") as scratch:
289
+ archive_path = Path(scratch) / "llama.zip"
290
+ try:
291
+ request = urllib.request.Request(
292
+ pin.url,
293
+ headers={"User-Agent": "distinct-agent", "Accept": "application/octet-stream"},
294
+ )
295
+ with fetch(request, timeout=60) as response:
296
+ declared = 0
297
+ try:
298
+ declared = int(response.headers.get("Content-Length") or 0)
299
+ except (TypeError, ValueError):
300
+ declared = 0
301
+ if declared and declared > MAX_ARCHIVE_BYTES:
302
+ raise RuntimeUnavailable(
303
+ "the llama.cpp download declares more than "
304
+ f"{MAX_ARCHIVE_BYTES // (1024 * 1024)} MB and was refused"
305
+ )
306
+ with archive_path.open("wb") as handle:
307
+ while True:
308
+ chunk = response.read(_CHUNK)
309
+ if not chunk:
310
+ break
311
+ handle.write(chunk)
312
+ digest.update(chunk)
313
+ written += len(chunk)
314
+ if written > MAX_ARCHIVE_BYTES:
315
+ raise RuntimeUnavailable(
316
+ "the llama.cpp download exceeded "
317
+ f"{MAX_ARCHIVE_BYTES // (1024 * 1024)} MB and was stopped"
318
+ )
319
+ if progress is not None and declared:
320
+ progress(
321
+ min(0.99, written / declared),
322
+ f"Fetching llama.cpp {pin.tag}",
323
+ )
324
+ except RuntimeUnavailable:
325
+ raise
326
+ except (OSError, urllib.error.URLError, ValueError) as exc:
327
+ raise RuntimeUnavailable(
328
+ f"could not fetch llama.cpp {pin.tag} for {resolved}: "
329
+ f"{type(exc).__name__}: {exc}. Point at a build you already have with "
330
+ "--llama-server, or start without a model using --demo-runner."
331
+ ) from exc
332
+
333
+ actual = digest.hexdigest()
334
+ if actual != pin.sha256:
335
+ raise RuntimeVerificationError(
336
+ f"the llama.cpp download for {resolved} does not match the recorded "
337
+ f"SHA-256: expected {pin.sha256}, computed {actual}. Nothing was "
338
+ "installed. This is what a substituted or corrupted build looks like; "
339
+ "report it rather than working around it."
340
+ )
341
+
342
+ if progress is not None:
343
+ progress(0.99, f"llama.cpp {pin.tag} verified, unpacking")
344
+
345
+ unpacked = Path(scratch) / "unpacked"
346
+ unpacked.mkdir()
347
+ try:
348
+ with zipfile.ZipFile(archive_path) as archive:
349
+ members = _safe_members(archive)
350
+ archive.extractall(unpacked, members=members)
351
+ except zipfile.BadZipFile as exc:
352
+ raise RuntimeUnavailable(
353
+ f"the llama.cpp download for {resolved} is not a readable archive: {exc}"
354
+ ) from exc
355
+
356
+ # The layout differs between releases, so the binaries are found rather
357
+ # than assumed, and copied flat into runtime/ where installed_server()
358
+ # and the existing _find_llama_server() both already look.
359
+ moved = 0
360
+ for source in sorted(unpacked.glob("**/*")):
361
+ if not source.is_file():
362
+ continue
363
+ destination = target / source.name
364
+ shutil.copy2(source, destination)
365
+ if not sys.platform.startswith("win"):
366
+ destination.chmod(destination.stat().st_mode | stat.S_IXUSR)
367
+ moved += 1
368
+
369
+ found = installed_server(root)
370
+ if not found:
371
+ raise RuntimeUnavailable(
372
+ f"the llama.cpp archive for {resolved} unpacked {moved} file(s) but none of "
373
+ "them was llama-server. The recorded asset is probably the wrong one; "
374
+ "report it rather than working around it."
375
+ )
376
+ if progress is not None:
377
+ progress(1.0, f"llama.cpp {pin.tag} installed in {target}")
378
+ return found
379
+
380
+
381
+ def describe_availability(key: str = "", pins_path: Optional[Path] = None) -> str:
382
+ """One line about what this machine can install, for the start-up report."""
383
+
384
+ resolved = key or platform_key()
385
+ pin = pin_for(resolved, pins_path)
386
+ if pin is None:
387
+ return f"llama.cpp: no build recorded for {resolved}; supply --llama-server"
388
+ return f"llama.cpp: {pin.tag} recorded for {resolved}, verified on install"
distinct_agent/transport.py CHANGED
@@ -42,6 +42,8 @@ from distinct_protocol.netpolicy import validate_server_url as policy_validate_s
42
  from .queue import OfferDecision
43
 
44
  PAIR_API = "/agent_pair"
 
 
45
  SYNC_API = "/agent_sync"
46
  ACCEPT_API = "/agent_accept"
47
  COMPLETE_API = "/agent_complete"
@@ -299,6 +301,93 @@ class GradioAgentTransport:
299
  self._last_snapshot: AgentSnapshot | None = None
300
  self._last_status: dict[str, JobStatus] = {}
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  @classmethod
303
  def pair(
304
  cls,
 
42
  from .queue import OfferDecision
43
 
44
  PAIR_API = "/agent_pair"
45
+ REGISTER_API = "/agent_register"
46
+ CLAIM_STATE_API = "/agent_claim_state"
47
  SYNC_API = "/agent_sync"
48
  ACCEPT_API = "/agent_accept"
49
  COMPLETE_API = "/agent_complete"
 
301
  self._last_snapshot: AgentSnapshot | None = None
302
  self._last_status: dict[str, JobStatus] = {}
303
 
304
+ @classmethod
305
+ def register(
306
+ cls,
307
+ server_url: str,
308
+ claim_code: str,
309
+ capabilities: AgentCapabilities,
310
+ *,
311
+ client_factory: Callable[[str], PredictClient] | None = None,
312
+ clock: Callable[[], float] = time.time,
313
+ nonce_factory: Callable[[], str] | None = None,
314
+ sleep: Callable[[float], None] = time.sleep,
315
+ max_attempts: int = 3,
316
+ retry_delay_seconds: float = 0.25,
317
+ allow_remote: bool | None = None,
318
+ allow_plaintext: bool | None = None,
319
+ verify_server: bool = True,
320
+ signing_public_key: bytes = b"",
321
+ access_code: str = "",
322
+ ) -> GradioAgentTransport:
323
+ """Register with a code this worker invented, to be claimed from the site.
324
+
325
+ The same handshake as :meth:`pair` with the code travelling the other
326
+ way. The transport that comes back is authenticated and usable, but the
327
+ server will offer it no work until somebody signed in claims it, so the
328
+ caller polls :meth:`claim_state` before settling into the work loop.
329
+
330
+ Attempted once, like pairing, and for a stronger reason: a retry that
331
+ succeeded after a lost response would leave two records waiting on one
332
+ code, and the server refuses the second rather than allowing the claim
333
+ to become ambiguous.
334
+ """
335
+
336
+ if not isinstance(capabilities, AgentCapabilities):
337
+ raise TypeError("capabilities must be AgentCapabilities")
338
+ server_url = validate_server_url(
339
+ server_url, allow_remote=allow_remote, allow_plaintext=allow_plaintext
340
+ )
341
+ factory = client_factory or _default_client_factory
342
+ client: PredictClient | None = None
343
+ try:
344
+ client = factory(server_url)
345
+ raw = client.predict(
346
+ claim_code,
347
+ canonical_body(capabilities.to_dict()),
348
+ bytes(signing_public_key).hex() if signing_public_key else "",
349
+ access_code or "",
350
+ api_name=REGISTER_API,
351
+ )
352
+ response = _decode_response(raw, rejection_type=AgentPairingRejected)
353
+ credential = _credential_from_response(response)
354
+ except AgentTransportError:
355
+ _close_quietly(client)
356
+ raise
357
+ except Exception as exc:
358
+ _close_quietly(client)
359
+ raise AgentTransportUnavailable(
360
+ f"register request failed: {type(exc).__name__}"
361
+ ) from exc
362
+ return cls(
363
+ client=client,
364
+ credential=credential,
365
+ clock=clock,
366
+ nonce_factory=nonce_factory,
367
+ sleep=sleep,
368
+ max_attempts=max_attempts,
369
+ retry_delay_seconds=retry_delay_seconds,
370
+ server_auth=(
371
+ ServerAuthenticator(decode_agent_secret(credential.secret))
372
+ if verify_server
373
+ else None
374
+ ),
375
+ )
376
+
377
+ def claim_state(self) -> str:
378
+ """``"waiting"``, ``"claimed"`` or ``"gone"``, for this worker's own record."""
379
+
380
+ try:
381
+ raw = self.client.predict(self.agent_id, api_name=CLAIM_STATE_API)
382
+ response = _decode_response(raw)
383
+ except AgentTransportError:
384
+ raise
385
+ except Exception as exc:
386
+ raise AgentTransportUnavailable(
387
+ f"claim state request failed: {type(exc).__name__}"
388
+ ) from exc
389
+ return str(response.get("claim_state", "waiting"))
390
+
391
  @classmethod
392
  def pair(
393
  cls,
distinct_server/api.py CHANGED
@@ -159,6 +159,74 @@ class AgentApi:
159
  except (ControlPlaneError, TypeError, ValueError) as exc:
160
  return _response({"ok": False, "error": str(exc)})
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  def sync(
163
  self,
164
  agent_id: str,
 
159
  except (ControlPlaneError, TypeError, ValueError) as exc:
160
  return _response({"ok": False, "error": str(exc)})
161
 
162
+ def register(
163
+ self,
164
+ claim_code: str,
165
+ capabilities_json: str,
166
+ signing_public_key_hex: str = "",
167
+ access_code: str = "",
168
+ ) -> str:
169
+ """Register a worker that will be claimed from the website afterwards.
170
+
171
+ The mirror image of :meth:`pair`. There, the code came from the server
172
+ and the volunteer carried it to the terminal; here the worker invents
173
+ one and the volunteer carries it to the browser. Nothing is copied out
174
+ of the page, which is what makes the failure this replaced -- somebody
175
+ inventing a value where the pairing code went -- impossible rather than
176
+ merely better explained.
177
+
178
+ This is the only entry point on the server that needs no account. What
179
+ it creates is inert: see ``ControlPlane.register_unclaimed``.
180
+ """
181
+
182
+ try:
183
+ capabilities = AgentCapabilities.from_dict(
184
+ _json_object(capabilities_json, maximum=65_536)
185
+ )
186
+ public_key = b""
187
+ if signing_public_key_hex:
188
+ if not isinstance(signing_public_key_hex, str):
189
+ raise ValueError("signing public key must be hexadecimal text")
190
+ try:
191
+ public_key = bytes.fromhex(signing_public_key_hex)
192
+ except ValueError as exc:
193
+ raise ValueError("signing public key is not valid hexadecimal") from exc
194
+ credential = self.control_plane.register_unclaimed(
195
+ capabilities,
196
+ claim_code,
197
+ signing_public_key=public_key,
198
+ access_code=access_code or "",
199
+ )
200
+ return _response(
201
+ {
202
+ "ok": True,
203
+ "agent_id": credential.agent_id,
204
+ "secret": credential.secret,
205
+ "issued_at": credential.issued_at,
206
+ "claim_state": "waiting",
207
+ "claim_ttl_s": self.control_plane.limits.claim_ttl_seconds,
208
+ "poll_interval_s": self.control_plane.limits.poll_interval_seconds,
209
+ "heartbeat_interval_s": self.control_plane.limits.poll_interval_seconds,
210
+ "offline_after_s": self.control_plane.limits.offline_after_seconds,
211
+ }
212
+ )
213
+ except (ControlPlaneError, TypeError, ValueError) as exc:
214
+ return _response({"ok": False, "error": str(exc)})
215
+
216
+ def claim_state(self, agent_id: str) -> str:
217
+ """Whether this worker has been claimed yet.
218
+
219
+ Says nothing a caller could not already know: a worker asks about the
220
+ record it created, and the answer is one of three words.
221
+ """
222
+
223
+ try:
224
+ return _response(
225
+ {"ok": True, "claim_state": self.control_plane.claim_state(str(agent_id or ""))}
226
+ )
227
+ except (ControlPlaneError, TypeError, ValueError) as exc:
228
+ return _response({"ok": False, "error": str(exc)})
229
+
230
  def sync(
231
  self,
232
  agent_id: str,
distinct_server/control_plane.py CHANGED
@@ -324,6 +324,167 @@ class ControlPlane:
324
  del self._pairing_codes[digest]
325
  return AgentCredential(agent_id, encode_secret(secret), current)
326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  def _phantom_record(self) -> AgentRecord:
328
  """A record for an agent that does not exist, so the checks can run.
329
 
@@ -833,6 +994,22 @@ class ControlPlane:
833
  # and its grantees, which is a worker that cannot be used sitting
834
  # in somebody's list looking like one that can.
835
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
  if not record.private:
837
  return True
838
  if not viewer_id:
@@ -875,7 +1052,12 @@ class ControlPlane:
875
  if not record.access_digest:
876
  continue
877
  if hmac.compare_digest(record.access_digest, digest):
878
- if record.revoked:
 
 
 
 
 
879
  raise AuthenticationError("that code is not valid")
880
  record.granted.add(viewer_id)
881
  return agent_id
@@ -890,7 +1072,7 @@ class ControlPlane:
890
  return tuple(
891
  agent_id
892
  for agent_id, record in self._agents.items()
893
- if viewer_id in record.granted
894
  )
895
 
896
  def list_agents(
@@ -1496,6 +1678,25 @@ class ControlPlane:
1496
  del self._pairing_codes[digest]
1497
  return len(expired)
1498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1499
  @staticmethod
1500
  def _cleanup_agent_nonces_locked(record: AgentRecord, now: float) -> int:
1501
  expired = [nonce for nonce, expires_at in record.nonces.items() if expires_at <= now]
 
324
  del self._pairing_codes[digest]
325
  return AgentCredential(agent_id, encode_secret(secret), current)
326
 
327
+ def register_unclaimed(
328
+ self,
329
+ capabilities: AgentCapabilities,
330
+ claim_code: str,
331
+ *,
332
+ signing_public_key: bytes = b"",
333
+ access_code: str = "",
334
+ private: bool = False,
335
+ now: Optional[float] = None,
336
+ ) -> AgentCredential:
337
+ """Register a worker that has not been claimed by anybody yet.
338
+
339
+ PAIRING RUNS THE OTHER WAY ROUND NOW, AND THIS IS WHY.
340
+
341
+ The old flow asked a volunteer to sign in, find a pairing code on the
342
+ page, and paste it into a terminal. The reliable failure was somebody
343
+ typing a value of their own where the code went and being told, one
344
+ program later, that their code was invalid -- which reads as the server
345
+ rejecting *them* rather than as a copy that never happened.
346
+
347
+ So the code travels the other way. The worker invents one, prints it,
348
+ and registers here with nothing but its digest. It is issued a
349
+ credential immediately, because it needs one to ask whether it has been
350
+ claimed yet, but the record it gets is inert: ``owner_id`` is empty and
351
+ ``claim_digest`` is set, which ``_visible_locked`` treats as "this
352
+ worker does not exist". Nothing can list it, select it or send it work.
353
+
354
+ That inertness is what makes an unauthenticated registration safe. What
355
+ remains is resource use, which is bounded two ways: a hard ceiling on
356
+ how many unclaimed records may exist at once, and a short expiry after
357
+ which they are swept.
358
+ """
359
+
360
+ if not isinstance(capabilities, AgentCapabilities):
361
+ raise ValidationError("capabilities must be AgentCapabilities")
362
+ with self._lock:
363
+ current = self._now(now)
364
+ self._cleanup_unclaimed_locked(current)
365
+ try:
366
+ digest = pairing_code_digest(claim_code, self._pairing_pepper)
367
+ except ValueError as exc:
368
+ raise ValidationError("claim code is not a usable code") from exc
369
+
370
+ unclaimed = sum(1 for record in self._agents.values() if record.claim_digest)
371
+ if unclaimed >= self.limits.max_unclaimed_agents:
372
+ raise CapacityError(
373
+ "too many workers are waiting to be claimed; try again shortly"
374
+ )
375
+ if len(self._agents) >= self.limits.max_agents:
376
+ raise CapacityError("agent registry is full")
377
+ if any(
378
+ hmac.compare_digest(record.claim_digest, digest)
379
+ for record in self._agents.values()
380
+ if record.claim_digest
381
+ ):
382
+ # Two workers offering one code would make the claim ambiguous,
383
+ # and the second one to arrive is the one that must lose.
384
+ raise ConflictError("that claim code is already waiting")
385
+
386
+ agent_id = capabilities.agent_id or _new_id("agent")
387
+ if agent_id in self._agents:
388
+ raise ConflictError("agent id is already registered")
389
+ if not capabilities.models:
390
+ raise ValidationError("agent must advertise at least one model")
391
+ if len(capabilities.models) > self.limits.max_models_per_agent:
392
+ raise CapacityError("agent advertises too many models")
393
+ if len(capabilities.tools) > self.limits.max_tools_per_agent:
394
+ raise CapacityError("agent advertises too many tools")
395
+ if not capabilities.energy_provider.strip():
396
+ raise ValidationError("agent must describe its energy provider")
397
+ if len(capabilities.energy_provider) > 256:
398
+ raise ValidationError("energy provider description is too long")
399
+ if not capabilities.agent_id:
400
+ capabilities = replace(capabilities, agent_id=agent_id)
401
+ if signing_public_key and (
402
+ not isinstance(signing_public_key, (bytes, bytearray))
403
+ or len(signing_public_key) != 32
404
+ ):
405
+ raise ValidationError("an Ed25519 public key is exactly 32 bytes")
406
+
407
+ secret = generate_secret()
408
+ self._agents[agent_id] = AgentRecord(
409
+ signing_public_key=bytes(signing_public_key),
410
+ owner_id="",
411
+ access_digest=(
412
+ access_code_digest(access_code, self._pairing_pepper)
413
+ if access_code
414
+ else ""
415
+ ),
416
+ capabilities=copy.deepcopy(capabilities),
417
+ secret=secret,
418
+ issued_at=current,
419
+ last_snapshot_at=current,
420
+ private=bool(private),
421
+ registered_capabilities=copy.deepcopy(capabilities),
422
+ claim_digest=digest,
423
+ claim_expires_at=current + self.limits.claim_ttl_seconds,
424
+ )
425
+ return AgentCredential(agent_id, encode_secret(secret), current)
426
+
427
+ def claim_agent(
428
+ self,
429
+ claim_code: str,
430
+ owner_id: str,
431
+ *,
432
+ now: Optional[float] = None,
433
+ ) -> str:
434
+ """Bind a waiting worker to the signed-in person who typed its code.
435
+
436
+ Returns the agent id. The failure message says nothing about why, for
437
+ the same reason ``redeem_access_code`` says nothing: an error that told
438
+ a caller apart "no such code" from "expired" from "already claimed"
439
+ would be an oracle, and this one can be asked without an account on the
440
+ machine that printed the code.
441
+
442
+ Signing in is the whole check. Ownership is what a claim confers -- the
443
+ right to be sent other people's prompts is downstream of it -- so it
444
+ cannot be granted to an identity the server has not authenticated.
445
+ """
446
+
447
+ if not isinstance(owner_id, str) or not owner_id.strip():
448
+ raise AuthenticationError("that code is not valid")
449
+ owner_id = _single_line_identifier(owner_id, "owner_id")
450
+ with self._lock:
451
+ current = self._now(now)
452
+ self._cleanup_unclaimed_locked(current)
453
+ try:
454
+ digest = pairing_code_digest(claim_code, self._pairing_pepper)
455
+ except ValueError as exc:
456
+ raise AuthenticationError("that code is not valid") from exc
457
+ for agent_id, record in self._agents.items():
458
+ if not record.claim_digest:
459
+ continue
460
+ if not hmac.compare_digest(record.claim_digest, digest):
461
+ continue
462
+ if record.revoked or record.claim_expires_at <= current:
463
+ raise AuthenticationError("that code is not valid")
464
+ record.owner_id = owner_id
465
+ # One use. Clearing the digest is also what makes the record
466
+ # visible, so the two can never disagree.
467
+ record.claim_digest = ""
468
+ record.claim_expires_at = 0.0
469
+ return agent_id
470
+ raise AuthenticationError("that code is not valid")
471
+
472
+ def claim_state(self, agent_id: str, *, now: Optional[float] = None) -> str:
473
+ """``"waiting"``, ``"claimed"``, or ``"gone"`` for a worker's own poll.
474
+
475
+ The worker asks about itself, having authenticated as itself, so this
476
+ tells it only what it already put there. It exists so a worker can stop
477
+ printing "waiting to be claimed" at the moment somebody claims it.
478
+ """
479
+
480
+ with self._lock:
481
+ current = self._now(now)
482
+ self._cleanup_unclaimed_locked(current)
483
+ record = self._agents.get(agent_id)
484
+ if record is None or record.revoked:
485
+ return "gone"
486
+ return "waiting" if record.claim_digest else "claimed"
487
+
488
  def _phantom_record(self) -> AgentRecord:
489
  """A record for an agent that does not exist, so the checks can run.
490
 
 
994
  # and its grantees, which is a worker that cannot be used sitting
995
  # in somebody's list looking like one that can.
996
  return False
997
+ if record.claim_digest:
998
+ # AN UNCLAIMED WORKER DOES NOT EXIST YET.
999
+ #
1000
+ # Registering without a pairing code is the one operation on this
1001
+ # server that needs no account, which is what lets a volunteer
1002
+ # start a worker and claim it afterwards. The price is that anybody
1003
+ # can create one of these records, so until a signed-in person
1004
+ # enters its claim code it must be visible to nobody and reachable
1005
+ # by nothing.
1006
+ #
1007
+ # This is the single place that has to be right. Every listing,
1008
+ # every selection, both dispatch paths and the per-agent view all
1009
+ # ask this function first, so one `False` here keeps an unclaimed
1010
+ # worker out of all of them at once rather than relying on six
1011
+ # call sites to remember.
1012
+ return False
1013
  if not record.private:
1014
  return True
1015
  if not viewer_id:
 
1052
  if not record.access_digest:
1053
  continue
1054
  if hmac.compare_digest(record.access_digest, digest):
1055
+ if record.revoked or record.claim_digest:
1056
+ # An unclaimed worker prints its access code before
1057
+ # anybody owns it. Honouring that code here would let a
1058
+ # stranger take a share of a machine whose operator has
1059
+ # not finished setting it up, so it grants nothing
1060
+ # until the worker is claimed.
1061
  raise AuthenticationError("that code is not valid")
1062
  record.granted.add(viewer_id)
1063
  return agent_id
 
1072
  return tuple(
1073
  agent_id
1074
  for agent_id, record in self._agents.items()
1075
+ if viewer_id in record.granted and not record.claim_digest
1076
  )
1077
 
1078
  def list_agents(
 
1678
  del self._pairing_codes[digest]
1679
  return len(expired)
1680
 
1681
+ def _cleanup_unclaimed_locked(self, now: float) -> int:
1682
+ """Forget workers nobody claimed in time.
1683
+
1684
+ An unclaimed record is the only thing on this server an unauthenticated
1685
+ caller can create, so it must not be something an unauthenticated caller
1686
+ can accumulate. It costs a slot for at most ``claim_ttl_seconds`` and is
1687
+ then gone; the worker holding it discovers this on its next poll and
1688
+ says so rather than waiting for ever.
1689
+ """
1690
+
1691
+ expired = [
1692
+ agent_id
1693
+ for agent_id, record in self._agents.items()
1694
+ if record.claim_digest and record.claim_expires_at <= now
1695
+ ]
1696
+ for agent_id in expired:
1697
+ del self._agents[agent_id]
1698
+ return len(expired)
1699
+
1700
  @staticmethod
1701
  def _cleanup_agent_nonces_locked(record: AgentRecord, now: float) -> int:
1702
  expired = [nonce for nonce, expires_at in record.nonces.items() if expires_at <= now]
distinct_server/models.py CHANGED
@@ -67,11 +67,21 @@ class ControlPlaneLimits:
67
  #: nobody else on the server can pair a worker. A per-owner share turns
68
  #: that from an outage into one account wasting its own allowance.
69
  max_pairing_codes_per_owner: int = 8
 
 
 
 
 
 
70
  max_nonces_per_agent: int = 2_048
71
  max_models_per_agent: int = 64
72
  max_tools_per_agent: int = 64
73
  max_result_characters: int = 1_000_000
74
  pairing_ttl_seconds: float = 600.0
 
 
 
 
75
  poll_interval_seconds: float = 5.0
76
  missed_polls_before_offline: int = 3
77
  # A floor on the offline threshold so that lowering the poll interval
@@ -90,6 +100,7 @@ class ControlPlaneLimits:
90
  "max_jobs_per_conversation",
91
  "max_pairing_codes",
92
  "max_pairing_codes_per_owner",
 
93
  "max_nonces_per_agent",
94
  "max_models_per_agent",
95
  "max_tools_per_agent",
@@ -98,6 +109,7 @@ class ControlPlaneLimits:
98
  )
99
  duration_fields = (
100
  "pairing_ttl_seconds",
 
101
  "poll_interval_seconds",
102
  "min_offline_seconds",
103
  "request_clock_skew_seconds",
@@ -259,6 +271,18 @@ class AgentRecord:
259
  #: one; that is an ordering question rather than a freshness question, and a
260
  #: counter answers it without the two sides agreeing a clock.
261
  snapshot_counter: int = 0
 
 
 
 
 
 
 
 
 
 
 
 
262
  #: The capability set registered at pairing, frozen. This is the ceiling an
263
  #: advertisement is intersected with. It must be the *pairing* set rather
264
  #: than the current one: an agent that advertises nothing while its
 
67
  #: nobody else on the server can pair a worker. A per-owner share turns
68
  #: that from an outage into one account wasting its own allowance.
69
  max_pairing_codes_per_owner: int = 8
70
+ #: How many workers may sit registered-but-unclaimed at once. An unclaimed
71
+ #: worker is the one thing here that anybody can create without signing in,
72
+ #: so it is the one thing that needs a ceiling and a short life. Both are
73
+ #: deliberately small: a claim is something a person does within a minute
74
+ #: of starting a worker, not something left pending.
75
+ max_unclaimed_agents: int = 64
76
  max_nonces_per_agent: int = 2_048
77
  max_models_per_agent: int = 64
78
  max_tools_per_agent: int = 64
79
  max_result_characters: int = 1_000_000
80
  pairing_ttl_seconds: float = 600.0
81
+ #: How long an unclaimed worker waits to be claimed before the server
82
+ #: forgets it. Long enough to walk to the browser, short enough that an
83
+ #: abandoned worker does not hold a slot.
84
+ claim_ttl_seconds: float = 900.0
85
  poll_interval_seconds: float = 5.0
86
  missed_polls_before_offline: int = 3
87
  # A floor on the offline threshold so that lowering the poll interval
 
100
  "max_jobs_per_conversation",
101
  "max_pairing_codes",
102
  "max_pairing_codes_per_owner",
103
+ "max_unclaimed_agents",
104
  "max_nonces_per_agent",
105
  "max_models_per_agent",
106
  "max_tools_per_agent",
 
109
  )
110
  duration_fields = (
111
  "pairing_ttl_seconds",
112
+ "claim_ttl_seconds",
113
  "poll_interval_seconds",
114
  "min_offline_seconds",
115
  "request_clock_skew_seconds",
 
271
  #: one; that is an ordering question rather than a freshness question, and a
272
  #: counter answers it without the two sides agreeing a clock.
273
  snapshot_counter: int = 0
274
+ #: Set while this worker is registered but not yet claimed by anybody.
275
+ #:
276
+ #: A worker started without a pairing code generates its own claim code,
277
+ #: prints it, and registers here holding only the digest. Until somebody
278
+ #: signed in enters that code, ``owner_id`` is empty and the record is
279
+ #: invisible to every viewer and every dispatch path -- see
280
+ #: ``ControlPlane._visible_locked``, which treats a non-empty claim digest
281
+ #: as "this worker does not exist yet". Claiming clears it.
282
+ claim_digest: str = ""
283
+ #: When an unclaimed record stops being claimable and is swept.
284
+ claim_expires_at: float = 0.0
285
+
286
  #: The capability set registered at pairing, frozen. This is the ceiling an
287
  #: advertisement is intersected with. It must be the *pairing* set rather
288
  #: than the current one: an agent that advertises nothing while its
distinct_server/render.py CHANGED
@@ -366,13 +366,23 @@ def join_this_server(
366
 
367
  # STEP FOUR: the run, with this server's address and this browser's own
368
  # library defaults already in it.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  code_note = (
370
- f'<p class="c-join__note">Pairing code, one use and ten minutes: '
371
- f'<code class="c-join__code">{esc(code)}</code></p>'
372
- if code
373
- else '<p class="c-join__note">Sign in at the top of this page and a pairing '
374
- "code appears here. Until then the command below is complete apart from "
375
- "that one value.</p>"
376
  )
377
  # One line on purpose. The previous form used backslash continuations,
378
  # which are bash syntax: pasted into PowerShell they became stray
@@ -380,7 +390,7 @@ def join_this_server(
380
  # into every shell anybody actually uses.
381
  arguments = (
382
  f"-m distinct_agent --server {first} "
383
- f"--pair {code or 'YOUR-CODE'} --models olmoe-1b-7b-0924-instruct --tools {tools or 'local'}"
384
  )
385
  run_it = _command_block(f"py -3 {arguments}", platform="win") + _command_block(
386
  f"python3 {arguments}", platform="nix"
@@ -401,9 +411,13 @@ def join_this_server(
401
  '<p class="c-step-num__head">Get something to run the models with</p>'
402
  f"{runner}</div></li>"
403
  '<li class="c-step-num"><span class="c-step-num__n">4</span>'
404
- '<div class="c-step-num__body"><p class="c-step-num__head">Point it at this server</p>'
405
  f'<code class="c-join__address">{esc(first)}</code>{alternates}{code_note}'
406
  f"{run_it}"
 
 
 
 
407
  f"{picture}</div></li>"
408
  # THE LAST STEP EXISTS BECAUSE PEOPLE ASK FOR IT AFTER READING THE ONE
409
  # BEFORE IT.
 
366
 
367
  # STEP FOUR: the run, with this server's address and this browser's own
368
  # library defaults already in it.
369
+ # NOTHING IS COPIED OUT OF THIS PAGE ANY MORE.
370
+ #
371
+ # This used to show a server-minted pairing code and a command with a
372
+ # `--pair YOUR-CODE` placeholder in it, and ask the volunteer to carry the
373
+ # one into the other. What people did instead -- reasonably, reading a
374
+ # placeholder as a thing to fill in however they liked -- was invent a
375
+ # value, and find out one program later that "your code wasn't valid",
376
+ # which reads as the server rejecting them rather than as a copy that never
377
+ # happened. Signed out, there was not even a code on the page to copy.
378
+ #
379
+ # The code now travels the other way: the worker invents one, prints it,
380
+ # and waits, and it is typed into the box on this page. There is nothing
381
+ # here to transcribe, so there is nothing to transcribe wrongly, and the
382
+ # command is complete for everybody whether they are signed in or not.
383
  code_note = (
384
+ '<p class="c-join__note">The command below is complete as it stands. '
385
+ "When you run it, the worker prints a code and waits.</p>"
 
 
 
 
386
  )
387
  # One line on purpose. The previous form used backslash continuations,
388
  # which are bash syntax: pasted into PowerShell they became stray
 
390
  # into every shell anybody actually uses.
391
  arguments = (
392
  f"-m distinct_agent --server {first} "
393
+ f"--models olmoe-1b-7b-0924-instruct --tools {tools or 'local'}"
394
  )
395
  run_it = _command_block(f"py -3 {arguments}", platform="win") + _command_block(
396
  f"python3 {arguments}", platform="nix"
 
411
  '<p class="c-step-num__head">Get something to run the models with</p>'
412
  f"{runner}</div></li>"
413
  '<li class="c-step-num"><span class="c-step-num__n">4</span>'
414
+ '<div class="c-step-num__body"><p class="c-step-num__head">Run it, and claim it</p>'
415
  f'<code class="c-join__address">{esc(first)}</code>{alternates}{code_note}'
416
  f"{run_it}"
417
+ '<p class="c-join__note">The worker prints a code and waits. Put that code '
418
+ "into <em>Add a worker</em> on this page while signed in, and the machine "
419
+ "becomes yours. Until you do, nobody can see it and nothing can be sent "
420
+ "to it.</p>"
421
  f"{picture}</div></li>"
422
  # THE LAST STEP EXISTS BECAUSE PEOPLE ASK FOR IT AFTER READING THE ONE
423
  # BEFORE IT.
distinct_server/ui.py CHANGED
@@ -2228,7 +2228,27 @@ class DistinctUI:
2228
  profile: "gr.OAuthProfile | None",
2229
  request: "gr.Request | None" = None,
2230
  ) -> tuple:
2231
- """Give this signed-in person access to the worker that printed a code.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2232
 
2233
  Signing in is checked first and separately from the code, because they
2234
  are two different refusals: one is "we do not know who you are" and the
@@ -2251,14 +2271,23 @@ class DistinctUI:
2251
  render.worker_cards(rows, narrowed_by=narrowed),
2252
  )
2253
 
 
2254
  try:
2255
- self.control.redeem_access_code(str(access_code or ""), viewer_id)
2256
  message = _status_html(
2257
- "That worker is now available to you. It appears below when it "
2258
- "is online and offers the model and mode you have chosen."
 
2259
  )
2260
  except (ControlPlaneError, ValueError):
2261
- message = _status_html("That code is not valid.")
 
 
 
 
 
 
 
2262
 
2263
  rows, choices, value, narrowed = self._agent_components(
2264
  model_id, tool_values, selected_agent, mode, viewer_id=viewer_id
@@ -3282,83 +3311,48 @@ class DistinctUI:
3282
  *,
3283
  force: bool = False,
3284
  ) -> tuple:
3285
- """The code for this browser: the one it already has, or a new one.
3286
-
3287
- MINTING ON EVERY OPEN WAS BOTH A BUG AND AN OUTAGE.
3288
-
3289
- The bug: a person opens the panel, copies the code, opens it again to
3290
- re-read the command, and is now looking at a different code from the
3291
- one in their clipboard.
3292
-
3293
- The outage: opening a section is cheap and scriptable, the pool of
3294
- live codes is shared by everybody on this server, and they last ten
3295
- minutes — so a loop over this handler fills the pool and stops anyone
3296
- else pairing a worker. The control plane now caps codes per owner as
3297
- well, which contains the damage; not minting one that nobody asked
3298
- for is what stops it starting.
3299
-
3300
- The plaintext lives in this browser's own state and nowhere else. The
3301
- server keeps only a digest, deliberately, so this is the sole copy —
3302
- which is exactly the right place for it, since this browser is the
3303
- only party entitled to read it.
3304
-
3305
- OPENING A SECTION IS NAVIGATION, SO IT CANNOT FAIL.
3306
-
3307
- This raised `gr.Error`, and the accordion that opens the panel is
3308
- wired straight to it, so a signed-out visitor who clicked "Run a
3309
- community agent" — the ordinary way to find out what running one
3310
- involves — got a red toast and a five hundred in the log for reading
3311
- a page. The other way in was the per-owner cap: eight live codes is
3312
- eight opens in a browser with no saved code, and the ninth open
3313
- refused with a toast rather than a sentence.
3314
-
3315
- Nothing here is worth a toast. The panel already reads correctly with
3316
- no code in it — it is four steps, and the code belongs to one of them —
3317
- so the reason a code was not minted is written into the panel, where
3318
- the person is already looking, and the section opens either way.
3319
  """
3320
 
3321
  updated = dict(state) if isinstance(state, Mapping) else {}
3322
- try:
3323
- user_id = self._authorize(state, profile, request)
3324
- except LoginRequired:
3325
- return (
3326
- updated,
3327
- updated,
3328
- _status_html(
3329
- "Sign in with Hugging Face to get a pairing code. The rest of "
3330
- "the setup is the same either way, so it is all here to read "
3331
- "first."
3332
- )
3333
- + self._join_panel("", updated),
3334
- )
3335
- existing = str(updated.get("pairing_code") or "")
3336
- expires = updated.get("pairing_expires")
3337
- alive = (
3338
- existing
3339
- and isinstance(expires, (int, float))
3340
- # A minute's margin: a code that expires while somebody is
3341
- # pasting it is worse than one re-minted slightly early.
3342
- and expires - 60 > time.time()
3343
- )
3344
- if alive and not force:
3345
- return updated, updated, self._join_panel(existing, updated)
3346
- try:
3347
- value = self.control.create_pairing_code(user_id)
3348
- except (ControlPlaneError, ValueError) as exc:
3349
- return (
3350
- updated,
3351
- updated,
3352
- _status_html(f"No new pairing code just now: {exc}")
3353
- + self._join_panel(existing, updated),
3354
- )
3355
- updated["pairing_code"] = value.code
3356
- updated["pairing_expires"] = value.expires_at
3357
- return updated, updated, self._join_panel(value.code, updated)
3358
 
3359
- # `new_pairing_code` lived here to serve a "New code" button that no
3360
- # longer exists. Opening the panel mints one when the held code has
3361
- # expired, which is the only moment a second one was ever wanted.
 
3362
 
3363
  def build(self) -> gr.Blocks:
3364
  # ``head`` belongs to launch() in Gradio 6, not to the constructor; the
@@ -3569,7 +3563,7 @@ class DistinctUI:
3569
  # and entering it here is that consent arriving.
3570
  with gr.Row():
3571
  access_code = gr.Textbox(
3572
- label="Add a worker · paste an access code somebody shared with you",
3573
  placeholder="XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXX",
3574
  lines=1,
3575
  container=True,
@@ -3577,10 +3571,11 @@ class DistinctUI:
3577
  access_add = gr.Button("Add worker", variant="secondary")
3578
  access_status = gr.HTML(
3579
  _status_html(
3580
- "A worker prints its code when it starts, and whoever "
3581
- "runs it decides who gets one. The same code can be "
3582
- "given to any number of people; entering it here does "
3583
- "not use it up."
 
3584
  )
3585
  )
3586
  # Boundary 1. Boundary 2 lives in its own panel far
@@ -4218,6 +4213,29 @@ class DistinctUI:
4218
  api_name="agent_pair",
4219
  queue=False,
4220
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4221
  signed_inputs = [api_agent_id, api_timestamp, api_nonce, api_payload, api_signature]
4222
  api_catalogue_button.click(
4223
  self.agent_api.catalogue,
 
2228
  profile: "gr.OAuthProfile | None",
2229
  request: "gr.Request | None" = None,
2230
  ) -> tuple:
2231
+ """Take a code and do whatever that code means.
2232
+
2233
+ ONE BOX, BECAUSE A PERSON HAS ONE CODE.
2234
+
2235
+ A worker prints two codes over its life: a **claim code** while it waits
2236
+ to be adopted, and an **access code** afterwards for the people its
2237
+ operator wants to share it with. Those are different things and they are
2238
+ entered by different people, but nobody standing at this field knows or
2239
+ cares which kind they are holding -- they were handed a code and told to
2240
+ put it in. Two fields would make choosing wrongly possible, and choosing
2241
+ wrongly would produce "that code is not valid" for a code that was
2242
+ perfectly valid in the box next door. That is the exact shape of the
2243
+ failure this whole change exists to remove.
2244
+
2245
+ So the claim is tried first and access second, and a code that is
2246
+ neither is refused in the words both refusals already used. Both are
2247
+ 192-bit random strings drawn from the same alphabet, so in principle one
2248
+ value could be both; at 2**-192 that is not a case worth designing
2249
+ around, and the order makes it deterministic if it ever happened.
2250
+ Holding a code is the authorisation in either reading, so trying both
2251
+ grants nothing that trying one would not.
2252
 
2253
  Signing in is checked first and separately from the code, because they
2254
  are two different refusals: one is "we do not know who you are" and the
 
2271
  render.worker_cards(rows, narrowed_by=narrowed),
2272
  )
2273
 
2274
+ typed = str(access_code or "")
2275
  try:
2276
+ self.control.claim_agent(typed, viewer_id)
2277
  message = _status_html(
2278
+ "That worker is yours. It is now running for you, and it appears "
2279
+ "below when it is online and offers the model and mode you have "
2280
+ "chosen. Share its access code with anyone else who should use it."
2281
  )
2282
  except (ControlPlaneError, ValueError):
2283
+ try:
2284
+ self.control.redeem_access_code(typed, viewer_id)
2285
+ message = _status_html(
2286
+ "That worker is now available to you. It appears below when it "
2287
+ "is online and offers the model and mode you have chosen."
2288
+ )
2289
+ except (ControlPlaneError, ValueError):
2290
+ message = _status_html("That code is not valid.")
2291
 
2292
  rows, choices, value, narrowed = self._agent_components(
2293
  model_id, tool_values, selected_agent, mode, viewer_id=viewer_id
 
3311
  *,
3312
  force: bool = False,
3313
  ) -> tuple:
3314
+ """Render the setup panel. Nothing is minted, and nothing can fail.
3315
+
3316
+ THIS HANDLER USED TO ISSUE A CREDENTIAL, AND THAT WAS THE PROBLEM.
3317
+
3318
+ Opening a section is navigation. This one minted a server-side pairing
3319
+ code every time it ran, which produced three separate faults:
3320
+
3321
+ * The code changed under people. Open the panel, copy the code, open it
3322
+ again to re-read the command, and the clipboard no longer matched the
3323
+ page.
3324
+ * It was an outage waiting to happen. Opening a section is cheap and
3325
+ scriptable, live codes came from one pool shared by everybody on this
3326
+ server, and they lasted ten minutes -- so a loop here filled the pool
3327
+ and stopped anyone else pairing a worker.
3328
+ * It could refuse. Signed out it raised, so clicking "Run a community
3329
+ agent" to find out what running one involves produced a red toast and
3330
+ a 500 in the log for the crime of reading a page.
3331
+
3332
+ Each was patched in turn -- reuse the browser's live code, cap codes per
3333
+ owner, catch the refusals -- and the patches were all correct and all
3334
+ beside the point. The panel needed a code only because the flow made a
3335
+ volunteer carry one from this page to their terminal, and that flow is
3336
+ gone: the worker prints its own code and the person types it into the
3337
+ box on this page instead.
3338
+
3339
+ So there is nothing to mint, nothing to run out of, nothing to expire
3340
+ under anybody, and nothing that needs an account to read. The signature
3341
+ keeps ``state`` and ``force`` because the accordion and the tests are
3342
+ wired to them, and both are now ignored.
 
 
 
 
 
3343
  """
3344
 
3345
  updated = dict(state) if isinstance(state, Mapping) else {}
3346
+ # Kept only so a browser that saved one before this change does not
3347
+ # carry it around for ever.
3348
+ updated.pop("pairing_code", None)
3349
+ updated.pop("pairing_expires", None)
3350
+ return updated, updated, self._join_panel("", updated)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3351
 
3352
+ # `new_pairing_code` and the "New code" button are both gone, and so is the
3353
+ # minting they existed to trigger. `ControlPlane.create_pairing_code` stays
3354
+ # for `--pair`, which scripted setups still use: there a code is minted and
3355
+ # consumed by the same program and nobody has to read anything.
3356
 
3357
  def build(self) -> gr.Blocks:
3358
  # ``head`` belongs to launch() in Gradio 6, not to the constructor; the
 
3563
  # and entering it here is that consent arriving.
3564
  with gr.Row():
3565
  access_code = gr.Textbox(
3566
+ label="Add a worker · enter the code your worker printed, or one somebody shared with you",
3567
  placeholder="XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXX",
3568
  lines=1,
3569
  container=True,
 
3571
  access_add = gr.Button("Add worker", variant="secondary")
3572
  access_status = gr.HTML(
3573
  _status_html(
3574
+ "A worker you started prints a code and waits here for "
3575
+ "you to enter it; that claims it, once. A worker somebody "
3576
+ "else runs prints a code they can give to any number of "
3577
+ "people, and entering that one does not use it up. Both "
3578
+ "go in this box."
3579
  )
3580
  )
3581
  # Boundary 1. Boundary 2 lives in its own panel far
 
4213
  api_name="agent_pair",
4214
  queue=False,
4215
  )
4216
+ # THE OTHER DIRECTION. `agent_pair` takes a code the server minted;
4217
+ # `agent_register` takes one the worker minted, and creates a record
4218
+ # that is inert until somebody signed in claims it. Same inputs, so
4219
+ # the same hidden controls carry both.
4220
+ api_pair_button.click(
4221
+ self.agent_api.register,
4222
+ inputs=[
4223
+ api_pair_code,
4224
+ api_pair_capabilities,
4225
+ api_pair_pubkey,
4226
+ api_pair_access,
4227
+ ],
4228
+ outputs=api_pair_output,
4229
+ api_name="agent_register",
4230
+ queue=False,
4231
+ )
4232
+ api_pair_button.click(
4233
+ self.agent_api.claim_state,
4234
+ inputs=[api_pair_code],
4235
+ outputs=api_pair_output,
4236
+ api_name="agent_claim_state",
4237
+ queue=False,
4238
+ )
4239
  signed_inputs = [api_agent_id, api_timestamp, api_nonce, api_payload, api_signature]
4240
  api_catalogue_button.click(
4241
  self.agent_api.catalogue,
llama_runtime.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_comment": [
3
+ "Pinned llama.cpp builds, one per platform, fetched and verified by",
4
+ "distinct_agent/runtime.py. A worker downloads the build recorded here and",
5
+ "refuses to install it unless the bytes hash to `sha256`.",
6
+ "",
7
+ "A platform with no entry gets no automatic download. That is deliberate:",
8
+ "this project already withholds a MODEL whose digest it cannot check, and",
9
+ "the program that executes those models is held to the same bar. Operators",
10
+ "on those platforms pass --llama-server, or run --demo-runner and offer the",
11
+ "library's tools and skills with no model at all.",
12
+ "",
13
+ "TO ADD OR MOVE A PIN: download the asset, hash it, and record the result",
14
+ "here. `scripts/record_llama_pin.py` does exactly that and prints the JSON.",
15
+ "Never copy a digest from a release page or an API response into this file:",
16
+ "a digest that arrived with the download proves only that the bytes were",
17
+ "not corrupted in transit, which TLS already said, and pinning it would",
18
+ "dress trust-on-first-use up as verification."
19
+ ],
20
+ "builds": {
21
+ "windows-x64": {
22
+ "tag": "b10742",
23
+ "asset": "llama-b10742-bin-win-cpu-x64.zip",
24
+ "url": "https://github.com/ggml-org/llama.cpp/releases/download/b10742/llama-b10742-bin-win-cpu-x64.zip",
25
+ "sha256": "a923d80953d618335ae0073233fdcfb93760dfde646e957786894259aba87d72",
26
+ "bytes": 18373032
27
+ }
28
+ }
29
+ }
scripts/record_llama_pin.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Record a llama.cpp build for `llama_runtime.json`, by downloading and hashing it.
2
+
3
+ The digest in that file has to be produced by someone who fetched the bytes and
4
+ hashed them. Copying a hash out of a release page or an API response would pin
5
+ the same origin that served the download, which proves nothing TLS had not
6
+ already said -- and this project withholds a *model* on exactly that reasoning.
7
+
8
+ Usage::
9
+
10
+ python scripts/record_llama_pin.py windows-x64 # newest build
11
+ python scripts/record_llama_pin.py linux-x64 --tag b10742
12
+
13
+ Prints a JSON fragment to paste into `llama_runtime.json`, and exits non-zero if
14
+ no matching asset was found. It never writes the file itself: adding a pin
15
+ should be a diff somebody reads.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import hashlib
22
+ import json
23
+ import sys
24
+ import urllib.request
25
+
26
+ RELEASES = "https://api.github.com/repos/ggml-org/llama.cpp/releases?per_page=%d"
27
+
28
+ #: What a platform's asset name looks like, and what must not be in it. The
29
+ #: exclusions pick the build that runs anywhere over the one that runs fastest:
30
+ #: an operator who wants CUDA or Metal passes --llama-server.
31
+ PATTERNS = {
32
+ "windows-x64": (("win", "x64"), ("cuda", "hip", "sycl", "arm64", "vulkan")),
33
+ "windows-arm64": (("win", "arm64"), ("cuda", "hip", "sycl")),
34
+ "linux-x64": (("ubuntu", "x64"), ("cuda", "hip", "sycl", "arm64", "vulkan")),
35
+ "macos-arm64": (("macos", "arm64"), ()),
36
+ "macos-x64": (("macos", "x64"), ()),
37
+ }
38
+
39
+
40
+ def _get(url: str) -> bytes:
41
+ request = urllib.request.Request(url, headers={"User-Agent": "distinct-pin"})
42
+ with urllib.request.urlopen(request, timeout=60) as response: # noqa: S310
43
+ return response.read()
44
+
45
+
46
+ def main(argv: list[str]) -> int:
47
+ parser = argparse.ArgumentParser(description=__doc__)
48
+ parser.add_argument("platform", choices=sorted(PATTERNS))
49
+ parser.add_argument("--tag", default="", help="a specific build tag, e.g. b10742")
50
+ parser.add_argument("--scan", type=int, default=15, help="how many releases to look through")
51
+ args = parser.parse_args(argv)
52
+
53
+ wanted, refused = PATTERNS[args.platform]
54
+ releases = json.loads(_get(RELEASES % args.scan))
55
+ for release in releases:
56
+ if args.tag and release.get("tag_name") != args.tag:
57
+ continue
58
+ for asset in release.get("assets", []):
59
+ name = str(asset.get("name", "")).casefold()
60
+ if not name.endswith(".zip"):
61
+ continue
62
+ if not all(part in name for part in wanted):
63
+ continue
64
+ if any(part in name for part in refused):
65
+ continue
66
+ url = asset["browser_download_url"]
67
+ print(f"downloading {asset['name']} ({asset.get('size', 0)} bytes)", file=sys.stderr)
68
+ payload = _get(url)
69
+ print(
70
+ json.dumps(
71
+ {
72
+ args.platform: {
73
+ "tag": release["tag_name"],
74
+ "asset": asset["name"],
75
+ "url": url,
76
+ "sha256": hashlib.sha256(payload).hexdigest(),
77
+ "bytes": len(payload),
78
+ }
79
+ },
80
+ indent=2,
81
+ )
82
+ )
83
+ return 0
84
+ print(
85
+ f"no asset matching {wanted} found in the last {args.scan} releases",
86
+ file=sys.stderr,
87
+ )
88
+ return 1
89
+
90
+
91
+ if __name__ == "__main__":
92
+ raise SystemExit(main(sys.argv[1:]))
tests/test_claim_flow.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Registering without an account must create something that can do nothing.
2
+
3
+ Pairing runs the other way round now: a worker invents a code, prints it, and
4
+ registers here, and somebody signed in enters that code to claim it. That makes
5
+ ``register_unclaimed`` the only entry point on this server that needs no
6
+ account, so every test below is about the same question -- what can a caller do
7
+ with a record nobody has claimed? The answer has to be "nothing".
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import pytest
13
+
14
+ from distinct_server.control_plane import UNASSIGNED_AGENT, ControlPlane
15
+ from distinct_server.models import (
16
+ AgentCapabilities,
17
+ AuthenticationError,
18
+ CapacityError,
19
+ ConflictError,
20
+ )
21
+ from distinct_server.security import new_access_code, new_pairing_code
22
+
23
+
24
+ def _caps(name: str = "worker") -> AgentCapabilities:
25
+ return AgentCapabilities(
26
+ agent_id="",
27
+ name=name,
28
+ os="linux",
29
+ arch="x64",
30
+ cpu="test",
31
+ ram_gb=8.0,
32
+ models=("m1",),
33
+ tools=(),
34
+ energy_provider="test meter",
35
+ )
36
+
37
+
38
+ @pytest.fixture()
39
+ def plane() -> ControlPlane:
40
+ return ControlPlane()
41
+
42
+
43
+ def test_an_unclaimed_worker_is_invisible_to_everyone(plane: ControlPlane) -> None:
44
+ plane.register_unclaimed(_caps(), new_pairing_code())
45
+ assert plane.list_agents() == ()
46
+ assert plane.list_agents(viewer_id="user:alice") == ()
47
+ assert plane.list_agents(viewer_id="user:mallory") == ()
48
+
49
+
50
+ def test_an_unclaimed_workers_access_code_grants_nothing(plane: ControlPlane) -> None:
51
+ """Otherwise a stranger takes a share of a machine still being set up."""
52
+
53
+ access = new_access_code()
54
+ plane.register_unclaimed(_caps(), new_pairing_code(), access_code=access)
55
+ with pytest.raises(AuthenticationError):
56
+ plane.redeem_access_code(access, "user:mallory")
57
+
58
+
59
+ def test_claiming_requires_an_account(plane: ControlPlane) -> None:
60
+ code = new_pairing_code()
61
+ plane.register_unclaimed(_caps(), code)
62
+ with pytest.raises(AuthenticationError):
63
+ plane.claim_agent(code, "")
64
+ with pytest.raises(AuthenticationError):
65
+ plane.claim_agent(code, " ")
66
+
67
+
68
+ def test_a_wrong_code_says_nothing_about_why(plane: ControlPlane) -> None:
69
+ plane.register_unclaimed(_caps(), new_pairing_code())
70
+ with pytest.raises(AuthenticationError) as caught:
71
+ plane.claim_agent(new_pairing_code(), "user:alice")
72
+ assert str(caught.value) == "that code is not valid"
73
+
74
+
75
+ def test_claiming_makes_the_worker_real(plane: ControlPlane) -> None:
76
+ code = new_pairing_code()
77
+ access = new_access_code()
78
+ credential = plane.register_unclaimed(_caps(), code, access_code=access)
79
+ assert plane.claim_state(credential.agent_id) == "waiting"
80
+
81
+ assert plane.claim_agent(code, "user:alice") == credential.agent_id
82
+ assert plane.claim_state(credential.agent_id) == "claimed"
83
+ assert len(plane.list_agents()) == 1
84
+ # and only now does its access code mean anything
85
+ assert plane.redeem_access_code(access, "user:bob") == credential.agent_id
86
+
87
+
88
+ def test_a_claim_code_is_used_once(plane: ControlPlane) -> None:
89
+ code = new_pairing_code()
90
+ plane.register_unclaimed(_caps(), code)
91
+ plane.claim_agent(code, "user:alice")
92
+ with pytest.raises(AuthenticationError):
93
+ plane.claim_agent(code, "user:bob")
94
+
95
+
96
+ def test_two_workers_cannot_wait_on_one_code(plane: ControlPlane) -> None:
97
+ """An ambiguous claim is worse than a refused registration."""
98
+
99
+ code = new_pairing_code()
100
+ plane.register_unclaimed(_caps("first"), code)
101
+ with pytest.raises(ConflictError):
102
+ plane.register_unclaimed(_caps("second"), code)
103
+
104
+
105
+ def test_an_unclaimed_worker_expires(plane: ControlPlane) -> None:
106
+ code = new_pairing_code()
107
+ credential = plane.register_unclaimed(_caps(), code, now=1000.0)
108
+ later = 1000.0 + plane.limits.claim_ttl_seconds + 1
109
+ assert plane.claim_state(credential.agent_id, now=later) == "gone"
110
+ with pytest.raises(AuthenticationError):
111
+ plane.claim_agent(code, "user:alice", now=later)
112
+
113
+
114
+ def test_unclaimed_registrations_have_a_ceiling(plane: ControlPlane) -> None:
115
+ """The one unauthenticated write on this server cannot be accumulated."""
116
+
117
+ made = 0
118
+ with pytest.raises(CapacityError):
119
+ for _ in range(plane.limits.max_unclaimed_agents + 10):
120
+ plane.register_unclaimed(_caps(), new_pairing_code())
121
+ made += 1
122
+ assert made == plane.limits.max_unclaimed_agents
123
+
124
+
125
+ def test_expiry_frees_the_ceiling(plane: ControlPlane) -> None:
126
+ for _ in range(plane.limits.max_unclaimed_agents):
127
+ plane.register_unclaimed(_caps(), new_pairing_code(), now=1000.0)
128
+ later = 1000.0 + plane.limits.claim_ttl_seconds + 1
129
+ plane.register_unclaimed(_caps(), new_pairing_code(), now=later)
130
+
131
+
132
+ def test_no_job_can_be_sent_to_an_unclaimed_worker(plane: ControlPlane) -> None:
133
+ """The visibility gate is what dispatch consults, so this is the proof."""
134
+
135
+ credential = plane.register_unclaimed(_caps(), new_pairing_code())
136
+ session = plane.create_session()
137
+ conversation = plane.create_conversation(session.session_id)
138
+ job = plane.submit_job(
139
+ session.session_id,
140
+ conversation.conversation_id,
141
+ "hello",
142
+ "m1",
143
+ viewer_id="user:alice",
144
+ )
145
+ # Nothing to run it, so it is stranded rather than handed to the worker
146
+ # that is sitting right there unclaimed.
147
+ assert job.spec.target_agent_id != credential.agent_id
148
+ assert job.spec.target_agent_id == UNASSIGNED_AGENT
tests/test_llama_runtime.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The runtime fetcher must be as careful as the weights fetcher.
2
+
3
+ It downloads an executable from another project and puts it on a volunteer's
4
+ machine, so every one of these is a property somebody would be harmed by losing:
5
+ the digest is checked before anything is installed, an archive cannot write
6
+ outside the runtime directory, and an archive cannot fill the disk.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import io
13
+ import json
14
+ import zipfile
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ from distinct_agent import runtime
20
+
21
+
22
+ def _archive(names=("llama-server.exe", "ggml.dll"), size=32, deflate=False) -> bytes:
23
+ buf = io.BytesIO()
24
+ mode = zipfile.ZIP_DEFLATED if deflate else zipfile.ZIP_STORED
25
+ with zipfile.ZipFile(buf, "w", mode) as handle:
26
+ for name in names:
27
+ handle.writestr(f"build/bin/{name}", b"x" * size)
28
+ return buf.getvalue()
29
+
30
+
31
+ def _opener(payload: bytes):
32
+ class _Response:
33
+ headers = {"Content-Length": str(len(payload))}
34
+
35
+ def __init__(self) -> None:
36
+ self._buffer = io.BytesIO(payload)
37
+
38
+ def read(self, size=-1):
39
+ return self._buffer.read(size)
40
+
41
+ def __enter__(self):
42
+ return self
43
+
44
+ def __exit__(self, *exc):
45
+ return False
46
+
47
+ return lambda request, timeout=None: _Response()
48
+
49
+
50
+ def _pins(tmp_path: Path, sha: str, key: str = "test-x64") -> Path:
51
+ path = tmp_path / "pins.json"
52
+ path.write_text(
53
+ json.dumps(
54
+ {
55
+ "builds": {
56
+ key: {
57
+ "tag": "bTEST",
58
+ "asset": "a.zip",
59
+ "url": "https://github.com/ggml-org/llama.cpp/releases/download/bTEST/a.zip",
60
+ "sha256": sha,
61
+ "bytes": 1,
62
+ }
63
+ }
64
+ }
65
+ )
66
+ )
67
+ return path
68
+
69
+
70
+ def test_a_verified_archive_installs_and_is_not_fetched_twice(tmp_path: Path) -> None:
71
+ payload = _archive()
72
+ root = tmp_path / "proj"
73
+ root.mkdir()
74
+ pins = _pins(tmp_path, hashlib.sha256(payload).hexdigest())
75
+
76
+ installed = runtime.ensure(root=root, key="test-x64", opener=_opener(payload), pins_path=pins)
77
+ assert Path(installed).is_file()
78
+ assert Path(installed).name.startswith("llama-server")
79
+
80
+ def refuse(*args, **kwargs):
81
+ raise AssertionError("an installed runtime must not be downloaded again")
82
+
83
+ assert runtime.ensure(root=root, key="test-x64", opener=refuse, pins_path=pins) == installed
84
+
85
+
86
+ def test_a_wrong_digest_installs_nothing(tmp_path: Path) -> None:
87
+ payload = _archive()
88
+ root = tmp_path / "proj"
89
+ root.mkdir()
90
+ with pytest.raises(runtime.RuntimeVerificationError):
91
+ runtime.ensure(
92
+ root=root, key="test-x64", opener=_opener(payload), pins_path=_pins(tmp_path, "0" * 64)
93
+ )
94
+ assert not list((root / "runtime").glob("*")) if (root / "runtime").exists() else True
95
+
96
+
97
+ def test_an_unrecorded_platform_is_never_downloaded(tmp_path: Path) -> None:
98
+ """The bar the catalogue sets for a model applies to the thing that runs it."""
99
+
100
+ payload = _archive()
101
+ root = tmp_path / "proj"
102
+ root.mkdir()
103
+ with pytest.raises(runtime.RuntimeUnavailable) as caught:
104
+ runtime.ensure(
105
+ root=root,
106
+ key="mystery-arch",
107
+ opener=_opener(payload),
108
+ pins_path=_pins(tmp_path, hashlib.sha256(payload).hexdigest()),
109
+ )
110
+ assert "will not download one" in str(caught.value)
111
+
112
+
113
+ def test_path_traversal_is_refused(tmp_path: Path) -> None:
114
+ buf = io.BytesIO()
115
+ with zipfile.ZipFile(buf, "w") as handle:
116
+ handle.writestr("../../escaped.txt", b"x")
117
+ payload = buf.getvalue()
118
+ root = tmp_path / "proj"
119
+ root.mkdir()
120
+ with pytest.raises(runtime.RuntimeUnavailable) as caught:
121
+ runtime.ensure(
122
+ root=root,
123
+ key="test-x64",
124
+ opener=_opener(payload),
125
+ pins_path=_pins(tmp_path, hashlib.sha256(payload).hexdigest()),
126
+ )
127
+ assert "unsafe path" in str(caught.value)
128
+ assert not (tmp_path / "escaped.txt").exists()
129
+
130
+
131
+ def test_a_decompression_bomb_is_refused(tmp_path: Path) -> None:
132
+ """Small on the wire, enormous on disk. The digest would have matched."""
133
+
134
+ payload = _archive(names=("llama-server",), size=runtime.MAX_UNPACKED_BYTES + 1, deflate=True)
135
+ assert len(payload) < 5 * 1024 * 1024
136
+ root = tmp_path / "proj"
137
+ root.mkdir()
138
+ with pytest.raises(runtime.RuntimeUnavailable) as caught:
139
+ runtime.ensure(
140
+ root=root,
141
+ key="test-x64",
142
+ opener=_opener(payload),
143
+ pins_path=_pins(tmp_path, hashlib.sha256(payload).hexdigest()),
144
+ )
145
+ assert "unpacks to more than" in str(caught.value)
146
+
147
+
148
+ def test_an_archive_without_a_server_is_refused(tmp_path: Path) -> None:
149
+ payload = _archive(names=("readme.txt",))
150
+ root = tmp_path / "proj"
151
+ root.mkdir()
152
+ with pytest.raises(runtime.RuntimeUnavailable):
153
+ runtime.ensure(
154
+ root=root,
155
+ key="test-x64",
156
+ opener=_opener(payload),
157
+ pins_path=_pins(tmp_path, hashlib.sha256(payload).hexdigest()),
158
+ )
159
+
160
+
161
+ @pytest.mark.parametrize(
162
+ "url,allowed",
163
+ [
164
+ ("https://github.com/ggml-org/llama.cpp/releases/download/b1/a.zip", True),
165
+ ("https://objects.githubusercontent.com/whatever", True),
166
+ ("http://github.com/ggml-org/llama.cpp/a.zip", False),
167
+ ("https://github.com.evil.example/a.zip", False),
168
+ ("https://example.com/a.zip", False),
169
+ ],
170
+ )
171
+ def test_redirects_leave_github_nowhere(url: str, allowed: bool) -> None:
172
+ assert runtime._RuntimeRedirects._permitted(url) is allowed
173
+
174
+
175
+ def test_the_shipped_pin_file_is_readable_and_pinned() -> None:
176
+ """Whatever is recorded must be a real 64-character digest over https."""
177
+
178
+ for key, pin in runtime.load_pins().items():
179
+ assert len(pin.sha256) == 64, key
180
+ assert int(pin.sha256, 16) >= 0, key
181
+ assert pin.url.startswith("https://"), key
182
+ assert pin.tag and pin.asset, key
183
+
184
+
185
+ def test_platform_key_is_stable() -> None:
186
+ assert runtime.platform_key("Windows", "AMD64") == "windows-x64"
187
+ assert runtime.platform_key("Linux", "x86_64") == "linux-x64"
188
+ assert runtime.platform_key("Darwin", "arm64") == "macos-arm64"