Download distinct_agent/cli.py from User1342/distinct: direct link, hf CLI and curl.
- Browser
- Download file 84.3 kB
-
https://huggingface.co/spaces/User1342/distinct/resolve/main/distinct_agent/cli.py
- Command line
-
hf download hf://spaces/User1342/distinct/distinct_agent/cli.py
-
curl -L -o cli.py https://huggingface.co/spaces/User1342/distinct/resolve/main/distinct_agent/cli.py
84.3 kB
| """Minimal CLI and injectable transport entry point for the local worker.""" | |
| from __future__ import annotations | |
| import argparse | |
| import ctypes | |
| import json | |
| import math | |
| import os | |
| import platform | |
| import re | |
| import sys | |
| import threading | |
| import time | |
| import uuid | |
| from collections.abc import Callable, Iterable | |
| from dataclasses import replace | |
| from pathlib import Path | |
| from urllib.parse import urlsplit | |
| # Before any project import: the project uses 3.10 syntax at module level | |
| # (PEP 604 unions in isinstance), so on an older interpreter the failure is | |
| # an unrelated-looking TypeError deep in an import. A person on Windows whose | |
| # ``python`` is a stray 3.9 deserves the actual sentence instead. | |
| if sys.version_info < (3, 10): # pragma: no cover - cannot run under test | |
| sys.exit( | |
| "distinct needs Python 3.10 or newer; this is " | |
| f"{sys.version_info.major}.{sys.version_info.minor} ({sys.executable}).\n" | |
| "On Windows, run: py -3.12 -m distinct_agent ...\n" | |
| "Elsewhere, run: python3.12 -m distinct_agent ..." | |
| ) | |
| from distinct_protocol import ( | |
| MODE_AGENT, | |
| MODE_SIMPLE, | |
| AgentCapabilities, | |
| JobResult, | |
| JobSpec, | |
| JobStatus, | |
| ) | |
| from distinct_protocol.netpolicy import ( | |
| ALLOW_PLAINTEXT_SERVER_ENV_VAR, | |
| ALLOW_REMOTE_SERVER_ENV_VAR, | |
| DEFAULT_SERVER_URL, | |
| announce_remote_server, | |
| is_loopback_host, | |
| ) | |
| from distinct_tools import ( | |
| INSTALLABLE_SPECS, | |
| REPOSITORY_SPECS, | |
| REPOSITORY_STATUS, | |
| operator_policy, | |
| registry_notes, | |
| ) | |
| from distinct_tools.approval import APPROVED_TOOLS_ENV_VAR | |
| from .access import announce_access_code, new_access_code | |
| from .energy import detect_energy_meter | |
| from .fanout import MultiServerTransport, ServerLink | |
| from .isolation import describe_request_isolation, describe_support | |
| from .models import DiscoveredModel, ModelVerificationError | |
| from .queue import DEFAULT_POLL_INTERVAL_SECONDS, InMemoryJobQueue, OfferDecision | |
| from .rlm import RlmUnavailable, build_inference_harness, prepare_rlm_runtime | |
| from .runners import DeterministicDemoRunner, LlamaCppRunner | |
| from .sandbox import GeneratedExecutionPolicy, check_deno_sandbox | |
| from .server_runner import LlamaServerRunner | |
| from .servers import ( | |
| REASON_DENIED_BY_AGENT, | |
| AllowanceError, | |
| AllowanceGate, | |
| Refusal, | |
| ServerRegistry, | |
| ) | |
| from .tools import advertised_tool_refs | |
| from .transport import ( | |
| AgentTransportError, | |
| GradioAgentTransport, | |
| PredictClient, | |
| ) | |
| from .weights import ( | |
| DEFAULT_CACHE_GB, | |
| WeightsCache, | |
| WeightsUnavailable, | |
| cache_bytes, | |
| describe_catalogue, | |
| fetchable_manifests, | |
| shared_cache_directory, | |
| ) | |
| from .worker import AgentTransport, LocalInferenceExecutor, WorkerLoop, WorkerLoopSettings | |
| class ConsoleTransport: | |
| """One-job transport used only for local smoke tests.""" | |
| def __init__(self, offer: JobSpec | None = None) -> None: | |
| self.offer = offer | |
| self.done = threading.Event() | |
| def poll_offer(self, snapshot): | |
| print(json.dumps({"snapshot": snapshot.to_dict()}, sort_keys=True)) | |
| offer, self.offer = self.offer, None | |
| return offer | |
| def poll_cancellations(self) -> Iterable[str]: | |
| return () | |
| def acknowledge_offer(self, decision: OfferDecision) -> None: | |
| print(json.dumps({"offer": decision.to_dict()}, sort_keys=True)) | |
| if not decision.accepted: | |
| self.done.set() | |
| def publish_status(self, job_id: str, status: JobStatus, reason=None) -> None: | |
| print( | |
| json.dumps( | |
| {"status": {"job_id": job_id, "status": status.value, "reason": reason}}, | |
| sort_keys=True, | |
| ) | |
| ) | |
| if status in {JobStatus.CANCELLED, JobStatus.FAILED}: | |
| self.done.set() | |
| def publish_result(self, result: JobResult) -> None: | |
| print(json.dumps({"result": result.to_dict()}, sort_keys=True)) | |
| self.done.set() | |
| def _runner_problem(args) -> str: | |
| """Why this machine cannot run a model yet, in words, or empty if it can. | |
| Written as an instruction rather than a diagnosis. "llama-server was not | |
| found. Supply --llama-server" told somebody who had just waited out a nine | |
| gigabyte download the name of a flag, not where to get the program that | |
| flag points at, and read as though the flag took no value — which it does, | |
| so the next thing they saw was an argparse usage dump. | |
| """ | |
| if args.llama_cli and not args.llama_server: | |
| from shutil import which | |
| if which(args.llama_cli) or Path(args.llama_cli).is_file(): | |
| return "" | |
| return ( | |
| f"llama-cli was not found at {args.llama_cli!r}. Give the path to the " | |
| "binary, or run with --demo-runner to start without a model." | |
| ) | |
| if args.llama_server and args.llama_server is not True: | |
| if Path(str(args.llama_server)).is_file(): | |
| return "" | |
| return ( | |
| f"llama-server was not found at {str(args.llama_server)!r}. Give the " | |
| "path to the binary, or run with --demo-runner to start without a model." | |
| ) | |
| if _resolve_llama_server(args): | |
| return "" | |
| from . import runtime as _runtime | |
| where = _runtime.platform_key() | |
| if all(_runtime.pin_for(key) is None for key in _runtime.preferred_keys()): | |
| # Nothing recorded for this platform, so nothing was attempted. Say | |
| # which platform, because the answer is "add a pin for it" and that is | |
| # a thing somebody can do. | |
| return ( | |
| "This worker has no llama.cpp to run a model with, and none is recorded\n" | |
| f"for {where}, so nothing was downloaded.\n" | |
| "\n" | |
| " An unverified build of the program that runs the models is the one\n" | |
| " thing this project will not install for you. Either point at a build\n" | |
| " you already have:\n" | |
| "\n" | |
| " --llama-server /path/to/llama-server\n" | |
| "\n" | |
| " or start without a model. The worker still joins, and still runs every\n" | |
| " tool and skill in the library; only the model's own replies are absent:\n" | |
| "\n" | |
| " --demo-runner\n" | |
| "\n" | |
| f" To record a build for {where}, run:\n" | |
| f" python scripts/record_llama_pin.py {where}\n" | |
| ) | |
| return ( | |
| "This worker has no llama.cpp to run a model with, and the pinned build\n" | |
| "could not be installed. The reason is above.\n" | |
| "\n" | |
| " Point at a build you already have:\n" | |
| "\n" | |
| " --llama-server /path/to/llama-server\n" | |
| "\n" | |
| " or start without a model. The worker still joins, and still runs every\n" | |
| " tool and skill in the library; only the model's own replies are absent:\n" | |
| "\n" | |
| " --demo-runner\n" | |
| ) | |
| def _resolve_llama_server(args) -> str: | |
| """The build this machine should run, installing it if that is what it takes. | |
| ORDER MATTERS, AND THE OLD ORDER PINNED A MACHINE TO ITS FIRST INSTALL. | |
| It was "anything already installed, then fetch". A volunteer whose first | |
| run predated accelerated builds -- or who joined before their card was | |
| recorded -- had a CPU build in ``runtime/``, so the answer was always yes, | |
| and they stayed on the CPU for ever. That is the state the benchmark was | |
| measured in: a CUDA laptop at three tokens a second. | |
| So the first question is now "is the *best* build for this machine already | |
| here", and only a no sends it to fetch. Anything installed, and then PATH, | |
| remain as the last resorts they always were. | |
| """ | |
| from . import runtime as _runtime | |
| for key in _runtime.preferred_keys(): | |
| found = _runtime.installed_server(key=key) | |
| if found: | |
| return found | |
| # Nothing installed for the best key this machine can use. Fetching it | |
| # is the point; if it cannot be fetched, the loop falls through to the | |
| # next-best key, and _obtain_llama_server reports why in passing. | |
| break | |
| return _obtain_llama_server(args) or _find_llama_server() | |
| def _find_llama_server() -> str: | |
| """Look in runtime/ first, then PATH. | |
| ``runtime/`` is where :func:`distinct_agent.runtime.ensure` installs the | |
| pinned build, so a worker that fetched one on a previous run finds it here | |
| and does not fetch again. | |
| """ | |
| from . import runtime as _runtime | |
| installed = _runtime.best_installed() | |
| if installed: | |
| return installed | |
| from shutil import which | |
| for name in ("llama-server", "llama-server.exe"): | |
| found = which(name) | |
| if found: | |
| return found | |
| return "" | |
| def _obtain_llama_server(args) -> str: | |
| """Fetch and verify the pinned llama.cpp for this machine, or say why not. | |
| Returns the path, or an empty string when nothing could be installed. The | |
| reasons are printed as they happen rather than collected, because the | |
| download is the slow part and a volunteer watching a progress line wants to | |
| know what it is for. | |
| """ | |
| from . import runtime as _runtime | |
| if getattr(args, "no_fetch_runtime", False): | |
| return "" | |
| wanted = _runtime.preferred_keys() | |
| pin = next((found for found in map(_runtime.pin_for, wanted) if found is not None), None) | |
| if pin is None: | |
| return "" | |
| how = "CPU" if pin.accelerator == "cpu" else f"{pin.accelerator.upper()}-accelerated" | |
| print( | |
| f"No llama.cpp on this machine. Fetching the pinned {how} build " | |
| f"({pin.tag}, {pin.bytes / (1024 * 1024):.0f} MB) and verifying it against " | |
| "its recorded SHA-256. This happens once.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| last = [-1] | |
| def report(fraction: float, message: str) -> None: | |
| percent = int(fraction * 100) | |
| if percent // 10 != last[0] // 10 or fraction >= 1.0: | |
| last[0] = percent | |
| print(f" llama.cpp: {message} ({percent}%)", file=sys.stderr, flush=True) | |
| try: | |
| return _runtime.ensure_best(progress=report) | |
| except _runtime.RuntimeVerificationError as exc: | |
| # A digest mismatch is not a transient failure and must not be retried | |
| # around: it is what a substituted build looks like. | |
| print(f"\n{exc}\n", file=sys.stderr, flush=True) | |
| return "" | |
| except _runtime.RuntimeUnavailable as exc: | |
| print(f" llama.cpp could not be installed: {exc}", file=sys.stderr, flush=True) | |
| return "" | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(description="Run a Distinct local inference worker") | |
| parser.add_argument( | |
| "--server", | |
| action="append", | |
| nargs="?", | |
| const=DEFAULT_SERVER_URL, | |
| default=[], | |
| metavar="URL", | |
| help=( | |
| "URL of a Distinct server this worker will accept work from. " | |
| f"Defaults to {DEFAULT_SERVER_URL} when given without a value. " | |
| "Repeatable: one machine can serve several servers at once, " | |
| "sharing one queue, so the capacity it advertises is the capacity " | |
| "it has. Give a --pair code for each, in the same order. A server " | |
| "on another machine is supported but must be opted into with " | |
| "--allow-remote-server, and must use HTTPS." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--list-servers", | |
| action="store_true", | |
| help="print the servers this worker has paired with, then exit", | |
| ) | |
| parser.add_argument( | |
| "--forget-server", | |
| action="append", | |
| default=[], | |
| metavar="URL", | |
| help=( | |
| "remove a paired server by URL and exit. Its credentials and the " | |
| "operator approval bound to them are deleted; joining it again " | |
| "means pairing and approving again. Repeatable." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--servers-file", | |
| default=None, | |
| metavar="PATH", | |
| help=( | |
| "where paired servers are remembered between runs. Defaults to a " | |
| "per-user file beside the weights cache. Contains credentials, so " | |
| "it is written readable only by you." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--allow-plaintext-server", | |
| action="store_true", | |
| help=( | |
| "permit a remote --server that is not HTTPS. Separate from " | |
| "--allow-remote-server because it is a separate risk: that one is " | |
| "about which machine, this one is about whether anything between " | |
| "you and it can read your prompts. Use it when the link is already " | |
| "encrypted underneath, such as a Tailscale or WireGuard address, " | |
| "and not on open wifi. Equivalent to " | |
| f"{ALLOW_PLAINTEXT_SERVER_ENV_VAR}=1." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--server-api", | |
| choices=("gradio", "http"), | |
| default="gradio", | |
| help=( | |
| "how to reach the server. 'gradio' is the desktop server, which " | |
| "serves its agent API through Gradio's client protocol. 'http' is " | |
| "for servers that are not Gradio, such as the phone app, which " | |
| "serve the same five signed endpoints as plain JSON over HTTP. " | |
| "The signing and the response verification are identical either " | |
| "way; only the framing differs." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--allow-remote-server", | |
| action="store_true", | |
| help=( | |
| "permit --server to name a machine other than this one. Its users " | |
| "can send prompts to this worker and will see its hardware and " | |
| f"energy telemetry. Equivalent to {ALLOW_REMOTE_SERVER_ENV_VAR}=1." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--pair", | |
| action="append", | |
| default=[], | |
| metavar="CODE", | |
| help=( | |
| "short-lived one-use pairing code from the server. Repeatable, " | |
| "matched to --server in the order both are given." | |
| ), | |
| ) | |
| parser.add_argument("--name", default=platform.node() or "Distinct worker") | |
| parser.add_argument("--agent-id", help="optional fixed agent id; the server assigns one by default") | |
| # Models are named, never located. See distinct_agent.weights for why the | |
| # old ``--model ID=PATH`` form was removed rather than kept alongside this. | |
| parser.add_argument( | |
| "--models", | |
| action="append", | |
| default=[], | |
| metavar="ID[,ID...]", | |
| help=( | |
| "model ids this worker is willing to run, from the catalogue " | |
| "(repeatable, or comma-separated). Defaults to every model that can " | |
| "be fetched and verified. Weights are downloaded on demand and cached." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--tools", | |
| action="append", | |
| default=[], | |
| metavar="REF[,REF...]", | |
| help=( | |
| "library members this worker will offer: exact refs such as " | |
| "create_pdf@1, or the bundle name 'local' for every installed " | |
| "member that opens no socket. Nothing is offered unless named here, " | |
| "and naming one is the operator approval the broker checks." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--list-tools", | |
| action="store_true", | |
| help="print the library members this worker could offer, then exit", | |
| ) | |
| parser.add_argument( | |
| "--fetch-on-demand", | |
| action="store_true", | |
| help=( | |
| "do not download the chosen models at start-up; fetch each one the " | |
| "first time a run needs it instead. The first such run then waits " | |
| "for the download." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--list-models", | |
| action="store_true", | |
| help="print the models this worker could offer, and why any are withheld, then exit", | |
| ) | |
| parser.add_argument( | |
| "--cache-dir", | |
| default=None, | |
| help=( | |
| "where fetched weights are cached. Defaults to a shared per-user " | |
| "location so every agent and tool on this machine reuses the same " | |
| "download rather than each keeping its own copy: " | |
| "DISTINCT_MODELS_DIR if set, else the Hugging Face cache named by " | |
| "HF_HOME, else the platform user cache." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--models-dir", | |
| dest="cache_dir_legacy", | |
| default=None, | |
| help="deprecated alias for --cache-dir", | |
| ) | |
| parser.add_argument( | |
| "--model-cache-gb", | |
| type=float, | |
| default=DEFAULT_CACHE_GB, | |
| metavar="GB", | |
| help=( | |
| "ceiling on the weights cache. When a fetch would exceed it, the " | |
| "least recently used model is evicted first. " | |
| f"Default {DEFAULT_CACHE_GB:g} GB." | |
| ), | |
| ) | |
| parser.add_argument("--model-id", help="model used by --demo-prompt") | |
| # Present only so that an operator running the old command gets a sentence | |
| # rather than argparse's "ambiguous option: --model". | |
| parser.add_argument( | |
| "--model", | |
| action="append", | |
| default=[], | |
| metavar="ID=PATH", | |
| help=argparse.SUPPRESS, | |
| ) | |
| parser.add_argument( | |
| "--without-energy-measurement", | |
| action="store_true", | |
| help=( | |
| "start even though this machine has no readable energy counter. " | |
| "Every run will report no energy figure. Off by default, because a " | |
| "worker that cannot measure cannot do the one thing this network is " | |
| "for." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--no-fetch-runtime", | |
| action="store_true", | |
| help=( | |
| "never download llama.cpp. The worker uses one already on this " | |
| "machine or refuses to run models, which is what an operator on a " | |
| "metered or air-gapped connection wants." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--llama-server", | |
| nargs="?", | |
| const=True, | |
| help=( | |
| "path to llama-server. This is the default runner: clean output, " | |
| "real token counts, streaming, and a resident model across queued " | |
| "requests. Auto-detected in runtime/ or on PATH." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--llama-cli", | |
| nargs="?", | |
| const=True, | |
| help="legacy per-request runner. Only used when --llama-server is absent.", | |
| ) | |
| parser.add_argument("--queue-capacity", type=int, default=4) | |
| parser.add_argument( | |
| "--poll-interval", | |
| type=float, | |
| default=None, | |
| help=( | |
| "seconds between polls; defaults to the interval the server advertises at " | |
| "pairing, clamped to [1, 30]. The poll carries the snapshot, so this is " | |
| "also the heartbeat rate." | |
| ), | |
| ) | |
| parser.add_argument("--demo-prompt", help="run one deterministic local smoke-test job") | |
| parser.add_argument( | |
| "--demo-runner", | |
| action="store_true", | |
| help="use deterministic inference (explicit test mode; no real model inference)", | |
| ) | |
| parser.add_argument( | |
| "--mode", | |
| dest="harness", | |
| choices=("agent", "simple", "dspy-rlm", "structured"), | |
| help=( | |
| "how this worker serves a request. 'agent' (the default) runs a " | |
| "DSPy RLM agent in the Deno sandbox: it reasons, calls tools and " | |
| "loops. 'simple' does one planning pass, runs whatever tools the " | |
| "model named, then one answering pass, which holds up on models too " | |
| "small to sustain an agent loop. 'dspy-rlm' and 'structured' are the " | |
| "internal names and remain accepted." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--harness", | |
| dest="harness", | |
| choices=("agent", "simple", "dspy-rlm", "structured"), | |
| help=argparse.SUPPRESS, | |
| ) | |
| parser.add_argument( | |
| "--prepare-rlm", | |
| action="store_true", | |
| help="prepare the lock-pinned RLM cache, verify cached-only execution, and exit", | |
| ) | |
| parser.add_argument( | |
| "--no-request-sandbox", | |
| action="store_true", | |
| help=( | |
| "run every request in the worker's own directories instead of " | |
| "giving each one a fresh, disposable sandbox. Only for diagnosing " | |
| "a sandbox problem: it removes the boundary between one user's " | |
| "request and the next." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--allow-generated-code", | |
| action="store_true", | |
| help="enable only after the strict Deno permission probe passes", | |
| ) | |
| parser.add_argument( | |
| "--guard-model", | |
| metavar="MODEL_ID", | |
| help=( | |
| "model used for the input/output safety screen, chosen by the " | |
| "agent operator at setup. Must be one of this worker's installed " | |
| "models. Defaults to the model already loaded for the run." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--no-guard", | |
| action="store_true", | |
| help="disable the input/output safety screen entirely (operator's choice)", | |
| ) | |
| parser.add_argument( | |
| "--dashboard", | |
| action="store_true", | |
| help=( | |
| "draw the live status screen instead of scrolling a log. This is " | |
| "what a double-clicked executable uses; it is a flag rather than a " | |
| "separate program so that a screen and a log are the same worker " | |
| "with the same checks, differing only in how they report." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--approve", | |
| action="store_true", | |
| help=( | |
| "record operator approval for the full choice set (the server's " | |
| "catalogue intersected with what this worker offers). Without it, " | |
| "an interactive console prompts; a non-interactive worker starts " | |
| "with nothing approved and refuses every job, which is the honest " | |
| "default rather than a failure." | |
| ), | |
| ) | |
| return parser | |
| def _open_registry(args) -> "ServerRegistry": | |
| """The persisted server registry, with a permissive declared allowance. | |
| ``--list-servers`` and ``--forget-server`` read and edit what is already | |
| on file; they do not pair with anything, so the declared allowance they | |
| are opened with bounds nothing and is deliberately empty rather than a | |
| guess at what this boot would have offered. | |
| """ | |
| from distinct_protocol.handshake import Allowance | |
| return ServerRegistry(Allowance(models=(), tools=()), path=_servers_file(args.servers_file)) | |
| def _list_servers(args) -> int: | |
| registry = _open_registry(args) | |
| ids = registry.server_ids() | |
| if not ids: | |
| print("This worker has not paired with any server.") | |
| print(f"Nothing is stored at {_servers_file(args.servers_file)}.") | |
| return 0 | |
| print(f"{len(ids)} server(s), remembered in {_servers_file(args.servers_file)}:") | |
| for server_id in ids: | |
| record = registry.require(server_id) | |
| approval = getattr(record, "approval", None) | |
| if approval is None: | |
| terms = "no approval on file, so it would be refused every job" | |
| else: | |
| models = ", ".join(approval.allowance.models) or "no models" | |
| tools = ", ".join(approval.allowance.tools) or "no tools" | |
| terms = f"models {models}; tools {tools}" | |
| print(f" {server_id}") | |
| print(f" url: {record.url}") | |
| print(f" approved: {terms}") | |
| print("\nUse --forget-server URL to leave one.") | |
| return 0 | |
| def _forget_servers(args) -> int: | |
| """Leave one or more servers, by URL, and say what was dropped. | |
| By URL rather than by server id, because the URL is what the operator | |
| typed and the id is something the server chose. A URL that matches | |
| nothing is an error rather than a silent success: an operator who | |
| mistypes it and sees nothing would reasonably believe they had left. | |
| """ | |
| registry = _open_registry(args) | |
| by_url = {registry.require(server_id).url: server_id for server_id in registry.server_ids()} | |
| unknown = [url for url in args.forget_server if url not in by_url] | |
| if unknown: | |
| print(f"Not paired with: {', '.join(unknown)}", file=sys.stderr) | |
| if by_url: | |
| print("Known servers:", file=sys.stderr) | |
| for url in sorted(by_url): | |
| print(f" {url}", file=sys.stderr) | |
| else: | |
| print("This worker has not paired with any server.", file=sys.stderr) | |
| return 2 | |
| for url in args.forget_server: | |
| server_id = by_url[url] | |
| registry.forget(server_id) | |
| print( | |
| f"Left {url} ({server_id}). Its credentials and the approval bound to " | |
| "them are gone; joining again means pairing and approving again." | |
| ) | |
| return 0 | |
| def _no_meter_message() -> str: | |
| """Why this machine cannot be a worker, and what would change that.""" | |
| from .energy import energy_backend_report | |
| lines = [ | |
| "This worker will not start, because it cannot measure the energy it uses.", | |
| "", | |
| " distinct exists to put a metered figure beside every answer. A worker", | |
| " with no readable counter would serve runs that report nothing, which is", | |
| " worse than not serving them.", | |
| "", | |
| " Backends tried on this machine:", | |
| ] | |
| for label, ok, reason in energy_backend_report(): | |
| mark = "works" if ok else "no" | |
| lines.append(f" [{mark:>5}] {label}") | |
| if not ok and reason: | |
| lines.append(f" {reason}") | |
| lines += [ | |
| "", | |
| " The usual fixes:", | |
| " * NVIDIA GPU: install the library with pip install nvidia-ml-py", | |
| " * AMD GPU: install ROCm, which ships the amdsmi package", | |
| " * Linux CPU: RAPL lives in /sys/class/powercap and is often root-only.", | |
| " Containers usually hide it; run on the host, or grant the path.", | |
| " * Windows CPU: the Energy Meter counter needs an Intel or AMD chip", | |
| " with a working EMI driver. A VM will not have one.", | |
| " * A laptop: unplug it. The battery is itself a meter, and this can", | |
| " read it while the machine is discharging.", | |
| " * A server: ipmitool, if the BMC implements DCMI.", | |
| "", | |
| " To run anyway, knowing every run will report no energy:", | |
| " --without-energy-measurement", | |
| ] | |
| return "\n".join(lines) | |
| def _server_pairs(urls, codes): | |
| """Match each --server to its --pair, or explain the mismatch. | |
| With no ``--pair`` values at all, every server is paired the other way | |
| round: the worker invents a claim code, prints it, and waits for somebody | |
| signed in to enter it. That is the path the website documents, because the | |
| old one -- fetch a code from the page, carry it to a terminal -- reliably | |
| produced a person typing a value of their own where the code went and being | |
| told one program later that their code was invalid. | |
| ``--pair`` is kept for scripted setups, where a code is minted and consumed | |
| by the same program and nobody reads anything. Positional matching, because | |
| it is the only scheme that reads correctly aloud: "this server, this code; | |
| that server, that code". A mismatch is an error rather than a best guess, | |
| since guessing would mean sending one server's code to another, which is a | |
| credential leak dressed up as convenience. | |
| """ | |
| if not codes: | |
| # NO --pair AT ALL IS THE ORDINARY CASE NOW. | |
| # | |
| # Every server gets its own freshly invented claim code, and the | |
| # operator types it into that server's page. `None` for a code is the | |
| # signal further down to register-and-wait rather than pair. | |
| return [(url, None) for url in urls] | |
| if len(codes) != len(urls): | |
| print( | |
| f"{len(urls)} --server value(s) and {len(codes)} --pair value(s). " | |
| "Either give no --pair at all, and each server will print a code for " | |
| "you to enter on its page, or give one code per server in the same " | |
| "order. Codes are never interchangeable between servers.", | |
| file=sys.stderr, | |
| ) | |
| return None | |
| seen: set[str] = set() | |
| pairs = [] | |
| for url, code in zip(urls, codes, strict=True): | |
| if url in seen: | |
| print(f"--server {url} was given twice.", file=sys.stderr) | |
| return None | |
| seen.add(url) | |
| pairs.append((url, code)) | |
| return pairs | |
| def _servers_file(explicit: str | None) -> Path: | |
| """Where paired servers are remembered between runs. | |
| Beside the weights cache, because both are per-user worker state and an | |
| operator who knows where one lives can find the other. It holds | |
| credentials, so :mod:`distinct_agent.servers` writes it private. | |
| """ | |
| if explicit: | |
| return Path(explicit) | |
| return shared_cache_directory().parent / "servers.json" | |
| def _register_and_wait( | |
| url, | |
| capabilities, | |
| *, | |
| access_code, | |
| client_factory, | |
| allow_remote, | |
| allow_plaintext, | |
| signing_public_key, | |
| poll_seconds: float = 3.0, | |
| announce=None, | |
| sleep=None, | |
| ): | |
| """Print a claim code for this server and wait until somebody enters it. | |
| Returns the transport once the worker has been claimed, or ``None`` if it | |
| was not claimed in time or the server refused the registration. | |
| The wait is deliberately visible. A worker that registered silently and sat | |
| there would look identical to one that had hung, and the whole point of | |
| this direction is that the person is looking at the terminal when the code | |
| appears. | |
| """ | |
| import time as _time | |
| from .access import new_access_code as _new_code | |
| say = announce or (lambda text: print(text, file=sys.stderr, flush=True)) | |
| nap = sleep or _time.sleep | |
| claim_code = _new_code() | |
| try: | |
| transport = GradioAgentTransport.register( | |
| url, | |
| claim_code, | |
| capabilities, | |
| access_code=access_code, | |
| client_factory=client_factory, | |
| allow_remote=allow_remote or None, | |
| allow_plaintext=allow_plaintext or None, | |
| signing_public_key=signing_public_key, | |
| ) | |
| except (AgentTransportError, TypeError, ValueError) as exc: | |
| say(f"Registering with {url} failed: {exc}") | |
| return None | |
| say("") | |
| say(" ┌─────────────────────────────────────────────────────────┐") | |
| say(" │ This worker is waiting to be claimed. │") | |
| say(" └─────────────────────────────────────────────────────────┘") | |
| say("") | |
| say(f" Sign in at {url}") | |
| say(' open "Run a community agent", and enter this code:') | |
| say("") | |
| say(f" {claim_code}") | |
| say("") | |
| say(" Nobody can use this machine until you do, and the code works") | |
| say(" only once. Waiting…") | |
| say("") | |
| deadline = _time.monotonic() + 15 * 60 | |
| while _time.monotonic() < deadline: | |
| try: | |
| state = transport.claim_state() | |
| except (AgentTransportError, TypeError, ValueError): | |
| # A poll that fails is not a claim that failed. Keep waiting; a | |
| # server that is really gone will still be gone at the deadline. | |
| state = "waiting" | |
| if state == "claimed": | |
| say(f"Claimed. {capabilities.name!r} is now serving {url} as {transport.agent_id}.") | |
| return transport | |
| if state == "gone": | |
| say( | |
| "This worker's registration expired before anybody claimed it. " | |
| "Nothing was left running on the server; start the worker again " | |
| "for a fresh code." | |
| ) | |
| return None | |
| nap(poll_seconds) | |
| say( | |
| "Nobody claimed this worker within fifteen minutes, so it has stopped " | |
| "waiting. Start it again for a fresh code." | |
| ) | |
| return None | |
| def _join_server( | |
| url, | |
| code, | |
| *, | |
| capabilities, | |
| manifests, | |
| client_factory, | |
| allow_remote, | |
| allow_plaintext, | |
| approve_flag, | |
| registry, | |
| access_code="", | |
| ): | |
| """Pair with one server, consent to it, and return a link and its gate. | |
| Returns ``(None, None)`` on any failure, having said why. A server that | |
| cannot be joined is not fatal to a worker that has others: the caller | |
| carries on, and refuses to start only if nothing was joined at all. | |
| """ | |
| from distinct_protocol.handshake import ( | |
| Allowance, | |
| OperatorApproval, | |
| ed25519_public_key, | |
| generate_ed25519_key, | |
| ) | |
| from .transport import decode_agent_secret as _decode | |
| # Say what a remote server is being granted before pairing gives it | |
| # anything, not after. | |
| host = urlsplit(url).hostname or "" | |
| if host and not is_loopback_host(host): | |
| announce_remote_server(url) | |
| declared = Allowance( | |
| models=tuple(manifest.id for manifest in manifests), | |
| tools=capabilities.tools, | |
| ) | |
| identity_key = generate_ed25519_key() | |
| if code is None: | |
| # THE CODE GOES FROM HERE TO THE BROWSER, NOT THE OTHER WAY. | |
| transport = _register_and_wait( | |
| url, | |
| capabilities, | |
| access_code=access_code, | |
| client_factory=client_factory, | |
| allow_remote=allow_remote, | |
| allow_plaintext=allow_plaintext, | |
| signing_public_key=ed25519_public_key(identity_key), | |
| ) | |
| if transport is None: | |
| return None, None | |
| else: | |
| try: | |
| transport = GradioAgentTransport.pair( | |
| url, | |
| code, | |
| capabilities, | |
| access_code=access_code, | |
| client_factory=client_factory, | |
| allow_remote=allow_remote or None, | |
| allow_plaintext=allow_plaintext or None, | |
| signing_public_key=ed25519_public_key(identity_key), | |
| ) | |
| except (AgentTransportError, TypeError, ValueError) as exc: | |
| print(f"Pairing with {url} failed: {exc}", file=sys.stderr) | |
| return None, None | |
| print( | |
| f"Paired {capabilities.name!r} with {url} as {transport.agent_id}.", | |
| file=sys.stderr, | |
| ) | |
| # -------- Consent handshake: catalogue, approval, gate ---------- | |
| # | |
| # Per server, and deliberately not shared. The server's signed catalogue | |
| # is pulled fresh, the operator approves a subset bound to that | |
| # catalogue's digest, the approved set is advertised in every signed | |
| # snapshot, and the AllowanceGate refuses any job outside it whatever the | |
| # server says. | |
| try: | |
| catalogue = transport.fetch_catalogue() | |
| registry.register( | |
| catalogue.server_id, | |
| url=url, | |
| agent_id=transport.agent_id, | |
| # Trust-on-first-use identity for this pairing: the exact | |
| # permission set the operator is about to be shown. | |
| pin=catalogue.digest(), | |
| secret=_decode(transport.credential.secret), | |
| offer=declared, | |
| identity_key=identity_key, | |
| # Rejoining a server already on file. Pairing codes are one-use, | |
| # so this path always carries fresh credentials, and the approval | |
| # bound to the old ones is dropped and re-taken below. | |
| replace_existing=True, | |
| ) | |
| choice = registry.record_catalogue(catalogue.server_id, catalogue) | |
| approved = _operator_approval(choice, approve_flag=approve_flag) | |
| if not approved: | |
| # Approving nothing remains a legitimate answer. Staying joined | |
| # afterwards is not: a worker that will refuse every job from this | |
| # server has nothing to contribute to it, and appearing online | |
| # while refusing everything is the failure mode that is invisible | |
| # from both ends. | |
| print( | |
| f"Nothing approved for {url}; not joining it.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| close = getattr(transport, "close", None) | |
| if callable(close): | |
| close() | |
| return None, None | |
| registry.approve( | |
| catalogue.server_id, | |
| OperatorApproval( | |
| server_id=catalogue.server_id, | |
| catalogue_digest=catalogue.digest(), | |
| allowance=choice, | |
| approved_at=time.time(), | |
| method="--approve flag" if approve_flag else "operator-console", | |
| ), | |
| ) | |
| print( | |
| f"Operator approved for {catalogue.server_id!r}: " | |
| f"models {list(choice.models)}, tools {list(choice.tools)}.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| gate = AllowanceGate(registry, catalogue.server_id) | |
| transport.snapshot_signer = ( | |
| lambda snapshot: registry.sign_snapshot(catalogue.server_id, snapshot) | |
| ) | |
| except (AgentTransportError, AllowanceError, TypeError, ValueError) as exc: | |
| print(f"Consent handshake with {url} failed: {exc}", file=sys.stderr) | |
| return None, None | |
| return ServerLink(name=catalogue.server_id, url=url, transport=transport), gate | |
| class _CombinedGate: | |
| """The right server's allowance gate for whichever server sent the job. | |
| Each server has its own approved set, and a job must be checked against | |
| the set approved for *its* server. A single merged gate would let a model | |
| approved for one server be run for another, which is exactly the | |
| disclosure the per-server consent handshake exists to prevent. | |
| """ | |
| def __init__(self, gates, transport) -> None: | |
| self._gates = dict(gates) | |
| self._transport = transport | |
| def evaluate(self, job): | |
| link = self._transport._link_for(job.id) # noqa: SLF001 - one module's pair | |
| gate = self._gates.get(link.name) if link is not None else None | |
| if gate is None: | |
| # A job whose server cannot be identified is refused. Falling back | |
| # to any gate would mean checking it against permissions somebody | |
| # granted to a different server. | |
| return Refusal(REASON_DENIED_BY_AGENT, "this job has no identifiable server") | |
| return gate.evaluate(job) | |
| def _advertised_mode(harness_name: str) -> str: | |
| """Which of the two agent modes a harness actually is. | |
| Four harness names exist internally and two of them are the same thing to | |
| a person choosing a worker: whether the model gets to see a tool's result | |
| before deciding the next call. That is the distinction worth advertising, | |
| so it is the one that crosses the wire. | |
| """ | |
| return MODE_SIMPLE if harness_name in {"simple", "structured"} else MODE_AGENT | |
| def main( | |
| argv: list[str] | None = None, | |
| *, | |
| transport: AgentTransport | None = None, | |
| client_factory: Callable[[str], PredictClient] | None = None, | |
| ) -> int: | |
| # No arguments at all, on a console somebody can answer: open the screen | |
| # rather than print an argument grammar. See distinct_agent/desktop.py for | |
| # why that is the right answer for this program and the wrong one for most. | |
| # | |
| # Guarded on the injection points as well as on argv: a test or an embedder | |
| # that hands in its own transport has been specific about what it wants, | |
| # and reading its terminal to decide would make that unpredictable. | |
| if argv is None and transport is None and client_factory is None: | |
| from .desktop import run as run_desktop | |
| from .desktop import wants_desktop | |
| if wants_desktop(): | |
| return run_desktop() | |
| args = build_parser().parse_args(argv) | |
| if client_factory is None and getattr(args, "server_api", "gradio") == "http": | |
| # Chosen, never inferred. A silent fall back from one transport to the | |
| # other would turn "this server speaks something else" into a mystery | |
| # timeout, and the two are not interchangeable from the server's side. | |
| from .httpapi import HttpPredictClient | |
| client_factory = HttpPredictClient | |
| if args.prepare_rlm: | |
| try: | |
| prepare_rlm_runtime() | |
| except (ImportError, OSError, RlmUnavailable, RuntimeError, ValueError) as exc: | |
| print(f"RLM runtime preparation failed: {exc}", file=sys.stderr) | |
| return 2 | |
| print("RLM runtime prepared; cached-only verification passed.") | |
| return 0 | |
| if args.server and args.demo_prompt is not None: | |
| print("--demo-prompt is a local smoke test and cannot be used with --server.", file=sys.stderr) | |
| return 2 | |
| if args.demo_prompt is not None and not args.demo_prompt.strip(): | |
| # Checked here rather than left to the protocol, which raises. An | |
| # operator who typed an empty string got a traceback ending in | |
| # "prompt cannot be empty", which is the right fact delivered as a | |
| # crash. Argparse-level mistakes deserve argparse-level answers. | |
| print("--demo-prompt needs something to ask; it was empty.", file=sys.stderr) | |
| return 2 | |
| if args.list_servers: | |
| return _list_servers(args) | |
| if args.forget_server: | |
| return _forget_servers(args) | |
| if args.pair and not args.server and transport is None: | |
| print("--pair requires --server.", file=sys.stderr) | |
| return 2 | |
| # Checked here, before anything is downloaded. A worker told to join two | |
| # servers with one code has been mis-invoked, and finding that out after a | |
| # four-hundred-megabyte download is a waste of somebody's evening. | |
| if args.server and transport is None and _server_pairs(args.server, args.pair) is None: | |
| return 2 | |
| if not 1 <= args.queue_capacity <= 64: | |
| print("--queue-capacity must be between 1 and 64.", file=sys.stderr) | |
| return 2 | |
| if args.poll_interval is not None and ( | |
| not math.isfinite(args.poll_interval) or not 0 < args.poll_interval <= 60 | |
| ): | |
| print("--poll-interval must be finite and between 0 and 60 seconds.", file=sys.stderr) | |
| return 2 | |
| if args.model: | |
| print( | |
| "--model ID=PATH has been removed. Weights are no longer configured by " | |
| "path: this worker resolves each model to its pinned repository and " | |
| "revision, downloads it on first use, and verifies it against the " | |
| "published digest before loading it. A path could do none of that.\n" | |
| "Use --models ID[,ID...] to choose which models to offer, and " | |
| "--list-models to see what is available.", | |
| file=sys.stderr, | |
| ) | |
| return 2 | |
| # The operator's library approval, applied before anything reads the | |
| # registry. It is written into the environment the approval layer already | |
| # reads rather than into a second mechanism beside it: one place decides | |
| # what is approved, and it is still a human naming it. | |
| if args.tools: | |
| os.environ[APPROVED_TOOLS_ENV_VAR] = ",".join(_requested_model_ids(args.tools)) | |
| if args.list_tools: | |
| offered = set(advertised_tool_refs()) | |
| named = { | |
| f"{ref.tool_id}@{ref.version}" for ref in operator_policy().approved | |
| } | |
| def show(ref: str, kind: str) -> None: | |
| # Three states, not two. A ref the operator approved and did not | |
| # get is not "not approved", and telling them it is sends them to | |
| # fix the one thing that is already right. | |
| if ref in offered: | |
| mark = "offered" | |
| elif ref in named: | |
| mark = "approved, not installed" | |
| else: | |
| mark = "not approved" | |
| print(f" {ref:<28} {kind:<6} {mark}") | |
| print("Library members, approvable one by one or all at once with 'local':") | |
| for spec in sorted(INSTALLABLE_SPECS, key=lambda item: (item.kind, item.tool_id)): | |
| show(f"{spec.tool_id}@{spec.version}", spec.kind) | |
| # The next two groups are deliberately outside the 'local' bundle: | |
| # each one has to be named in full. distinct_tools' module docstring | |
| # says why, and the short version is that one word should not approve | |
| # a skill read off the disk or a tool that writes files. | |
| print("\nSkills from the on-disk repository, each named in full to approve:") | |
| if REPOSITORY_SPECS: | |
| for spec in sorted(REPOSITORY_SPECS, key=lambda item: item.tool_id): | |
| show(f"{spec.tool_id}@{spec.version}", spec.kind) | |
| else: | |
| print(f" none available: {REPOSITORY_STATUS}") | |
| print("\nMCP servers, each tool named in full to approve:") | |
| try: | |
| from distinct_mcp import FILE_ROOT_ENV_VAR, catalogue_refs | |
| except ImportError as exc: # pragma: no cover - part of this repository | |
| print(f" none available: distinct_mcp could not be imported ({exc})") | |
| else: | |
| for ref in catalogue_refs(): | |
| show(ref, "mcp") | |
| print( | |
| f" the files.* tools also need {FILE_ROOT_ENV_VAR} set to the one " | |
| "directory they may use; there is no default" | |
| ) | |
| notes = registry_notes() | |
| if notes: | |
| print("\nApproved but not installed, so every call to it fails closed:") | |
| for note in notes: | |
| print(f" {note}") | |
| if not offered: | |
| print( | |
| "\nNothing is approved, so every tool call fails closed. Pass " | |
| "--tools local to offer every member that opens no socket, or " | |
| "name exact refs." | |
| ) | |
| return 0 | |
| offerable, withheld = describe_catalogue() | |
| if args.list_models: | |
| print("Models this worker can offer (weights fetched on demand):") | |
| for line in offerable: | |
| print(f" {line}") | |
| if withheld: | |
| print("\nWithheld, and why:") | |
| for line in withheld: | |
| print(f" {line}") | |
| return 0 | |
| # The catalogue, not the filesystem, decides what may be offered. A model | |
| # is advertisable when its repository, revision and digest are all pinned; | |
| # the file itself arrives the first time a run needs it. | |
| available = {manifest.id: manifest for manifest in fetchable_manifests()} | |
| requested = _requested_model_ids(args.models) | |
| if requested: | |
| unknown = [model_id for model_id in requested if model_id not in available] | |
| if unknown: | |
| print( | |
| f"Not offerable: {', '.join(unknown)}. Run --list-models to see what " | |
| "this worker can offer and why anything is withheld.", | |
| file=sys.stderr, | |
| ) | |
| return 2 | |
| manifests = tuple(available[model_id] for model_id in requested) | |
| else: | |
| manifests = tuple(available.values()) | |
| if not manifests: | |
| print( | |
| "No model in the catalogue has a pinned repository, revision and digest, " | |
| "so this worker has nothing it can honestly offer.", | |
| file=sys.stderr, | |
| ) | |
| return 2 | |
| if withheld: | |
| # Printed, not filtered away in silence: an operator who expected a | |
| # model to be here should read why it is not on the same screen. | |
| print("Models withheld from this worker:", file=sys.stderr) | |
| for line in withheld: | |
| print(f" {line}", file=sys.stderr) | |
| cache_directory = args.cache_dir or args.cache_dir_legacy or shared_cache_directory() | |
| try: | |
| weights = WeightsCache( | |
| cache_directory, | |
| limit_bytes=cache_bytes(args.model_cache_gb), | |
| manifests=manifests, | |
| ) | |
| models = weights.installed() | |
| except ModelVerificationError as exc: | |
| print(f"Model verification failed: {exc}", file=sys.stderr) | |
| return 2 | |
| except (OSError, TypeError, ValueError) as exc: | |
| print(f"Invalid weights cache configuration: {exc}", file=sys.stderr) | |
| return 2 | |
| if args.demo_runner or args.demo_prompt is not None: | |
| # A smoke test performs no inference, so it must not download hundreds | |
| # of megabytes to prove that. The deterministic runner never opens the | |
| # path, and the cache is withheld from the executor so nothing can. | |
| models = tuple( | |
| DiscoveredModel(manifest, Path(manifest.filename), 0, False) | |
| for manifest in manifests | |
| ) | |
| weights = None | |
| usage_source = weights | |
| pending = ( | |
| [manifest.id for manifest in manifests if not weights.is_cached(manifest)] | |
| if usage_source is not None | |
| else [] | |
| ) | |
| if usage_source is not None: | |
| usage = usage_source.usage() | |
| print( | |
| f"Weights cache: {cache_directory} " | |
| f"({usage.used_bytes / (1024**3):.2f} of {args.model_cache_gb:g} GB used). " | |
| f"Verified and ready: {', '.join(usage.model_ids) or 'none'}. " | |
| f"Fetched on first use: {', '.join(pending) or 'none'}.", | |
| flush=True, | |
| ) | |
| # DOWNLOAD AT START-UP, NOT MID-RUN. | |
| # | |
| # The operator's gesture is "I will host these models"; the download is | |
| # what that gesture means, so it happens when they make it. Deferring it | |
| # to the first job puts a several-hundred-megabyte wait inside somebody | |
| # else's request, on a worker that advertised itself as ready, and if it | |
| # fails it fails as a failed run rather than as a worker that could not | |
| # start. | |
| # CHECK FOR THE THING THAT RUNS THE MODEL BEFORE FETCHING THE MODEL. | |
| # | |
| # The runner was resolved after this block, so a machine without | |
| # llama.cpp downloaded nine gigabytes of weights, verified both digests, | |
| # printed its sandbox report, and only then said it had nothing to run | |
| # them with. Every second of that was avoidable: whether `llama-server` | |
| # exists is knowable before the first byte. | |
| if not (args.demo_runner or args.demo_prompt is not None): | |
| problem = _runner_problem(args) | |
| if problem: | |
| print(problem, file=sys.stderr) | |
| return 2 | |
| if pending and not args.fetch_on_demand and not (args.demo_runner or args.demo_prompt is not None): | |
| print( | |
| f"Fetching {len(pending)} model(s) now: {', '.join(pending)}. " | |
| "This happens once per model.", | |
| flush=True, | |
| ) | |
| for manifest in manifests: | |
| if manifest.id not in pending: | |
| continue | |
| report = _download_reporter(manifest.id) | |
| try: | |
| weights.ensure(manifest, progress=report) | |
| except ModelVerificationError as exc: | |
| print(f"Weights verification failed: {exc}", file=sys.stderr) | |
| return 2 | |
| except WeightsUnavailable as exc: | |
| print( | |
| f"Could not obtain {manifest.id}: {exc}\n" | |
| "Nothing was substituted. Fix the network or drop this model " | |
| "from --models, then start the worker again.", | |
| file=sys.stderr, | |
| ) | |
| return 2 | |
| print(f" {manifest.id}: verified against its published digest.", flush=True) | |
| models = weights.installed() | |
| model_id = args.model_id or manifests[0].id | |
| if model_id not in {manifest.id for manifest in manifests}: | |
| print(f"Requested model is not offered by this worker: {model_id}", file=sys.stderr) | |
| return 2 | |
| # Say what the OS will and will not enforce, every start-up. Silence here | |
| # would let an operator assume the inference child is contained when on | |
| # most hosts it is not. | |
| request_report = describe_request_isolation() | |
| if args.no_request_sandbox: | |
| print( | |
| "Per-request sandbox: DISABLED by --no-request-sandbox. Every request " | |
| "runs in this worker's own directories, and what one request leaves " | |
| "behind is visible to the next.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| elif request_report.mechanism == "none": | |
| print( | |
| f"Per-request sandbox: NONE. {request_report.reason} Each request still " | |
| "gets a fresh working directory and a cleared environment, but nothing " | |
| "at the OS layer stops it reading what your account can read.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| else: | |
| # This used to say each request got "a new process, no network and a | |
| # cleared environment". It did not. `RequestSandbox.spawn`, the method | |
| # that applies those, has no caller outside the tests: the worker | |
| # builds a sandbox, takes a scratch directory and a report from it, and | |
| # runs inference through the ordinary runner. The directory and its | |
| # erasure were real; the process, the network denial and the cleared | |
| # environment were a sentence. | |
| # | |
| # A start-up banner is the one place an operator looks to find out what | |
| # is protecting their machine, so it says what is applied and names | |
| # what is not. Applying the rest is a change to the run path, not to | |
| # this line, and until somebody makes it this must not read as though | |
| # somebody has. | |
| print( | |
| f"Per-request sandbox: {request_report.mechanism} is available, and is " | |
| "used for the fresh working directory each request gets, erased when " | |
| "the request ends. It does NOT yet wrap the model process: inference " | |
| "runs in the ordinary way, so nothing at the OS layer stops it " | |
| "reading what your account can read or reaching the network.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| isolation_report = describe_support() | |
| if isolation_report.applied: | |
| # flush=True: stdout is block-buffered when redirected to a file or a | |
| # log, and a security-relevant status line that appears minutes late, | |
| # or not at all if the process is killed, is worse than useless. | |
| print( | |
| f"Process restriction: {isolation_report.mechanism} active " | |
| f"(max {isolation_report.limits.get('max_processes')} process, " | |
| f"{isolation_report.limits.get('memory_bytes', 0) // (1024 * 1024)} MiB, " | |
| "killed with the agent). Containment and resource control, not a " | |
| "security sandbox: the model process still runs under your user account.", | |
| flush=True, | |
| ) | |
| else: | |
| print( | |
| f"Process restriction: NONE. {isolation_report.reason} " | |
| "The local model process runs with your full user privileges.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| # Detected before the runner is built: the server runner uses it to price | |
| # model loading, which is charged to residency rather than to a run. | |
| energy_meter = detect_energy_meter() | |
| if not energy_meter.available and not args.without_energy_measurement: | |
| # A WORKER THAT CANNOT MEASURE DOES NOT START. | |
| # | |
| # This project's claim is that the energy figure beside an answer was | |
| # metered on the machine that produced it. A worker with no readable | |
| # counter cannot honour that: every run it serves would carry an | |
| # absence, and a network of those is a network that measures nothing | |
| # while looking like one that does. | |
| # | |
| # So it stops here, before pairing, before downloading nine gigabytes | |
| # of weights, and before anybody has been told this machine is | |
| # available. And it stops with the whole list rather than a verdict, | |
| # because "no energy counter" is not something an operator can act on | |
| # and "the EMI driver is absent but the Energy Meter counter would work | |
| # if you were not in a container" is. | |
| print(_no_meter_message(), file=sys.stderr, flush=True) | |
| return 2 | |
| if energy_meter.available: | |
| print(f"Energy: {energy_meter.scope} via {energy_meter.provider}.", | |
| file=sys.stderr, flush=True) | |
| else: | |
| print( | |
| "Energy: NOT MEASURED on this machine, and you asked to continue anyway.\n" | |
| " Every run this worker serves will report no energy figure. It is\n" | |
| " counted as a run and never as zero, and the server shows the absence.", | |
| file=sys.stderr, flush=True, | |
| ) | |
| deno_status = check_deno_sandbox() if args.allow_generated_code else None | |
| generated_policy = GeneratedExecutionPolicy( | |
| enabled=args.allow_generated_code, | |
| status=deno_status, | |
| ) | |
| if args.allow_generated_code and not generated_policy.allowed: | |
| print( | |
| f"Generated execution remains disabled: {deno_status.reason if deno_status else 'not checked'}", | |
| file=sys.stderr, | |
| ) | |
| return 2 | |
| demo_runner = bool(args.demo_runner or args.demo_prompt is not None) | |
| if demo_runner: | |
| runner = DeterministicDemoRunner() | |
| elif args.llama_server or not args.llama_cli: | |
| # `--llama-server` with no value means "look for it"; the finder is | |
| # what a bare flag asks for, and passing True into a path would have | |
| # been a confusing failure two screens later. | |
| if args.llama_server is True: | |
| args.llama_server = "" | |
| # llama-server is the default. It returns clean output with no chat | |
| # furniture, reports real token counts, streams, keeps the model | |
| # resident across a queue, and speaks the OpenAI-compatible protocol | |
| # DSPy expects. --llama-cli remains for operators who want the old | |
| # per-request subprocess. | |
| runner = LlamaServerRunner( | |
| args.llama_server or _resolve_llama_server(args), | |
| energy_meter=energy_meter, | |
| ) | |
| # SAY WHICH DEVICE WILL DO THE WORK, AND SAY THAT IT WAS MEASURED. | |
| # | |
| # A volunteer with a graphics card who sees it sitting idle assumes | |
| # something is broken. On the laptop this was developed on the card | |
| # really is the wrong device -- 0.50 tokens per second against 2.93 on | |
| # the CPU, because a partly offloaded model pays a round trip per | |
| # token -- and the first time a model is loaded the worker spends a | |
| # few minutes finding that out. Both are worth saying out loud. | |
| runner._notify = lambda message: print(f" {message}", file=sys.stderr, flush=True) | |
| print(f"Runtime: {runner.executable}", file=sys.stderr, flush=True) | |
| if not runner.available: | |
| print( | |
| "llama-server was not found. Supply --llama-server, or --llama-cli " | |
| "for the legacy per-request runner, or --demo-runner.", | |
| file=sys.stderr, | |
| ) | |
| return 2 | |
| else: | |
| runner = LlamaCppRunner("" if args.llama_cli is True else args.llama_cli) | |
| if not runner.available: | |
| print( | |
| "llama-cli was not found; supply --llama-cli or use --demo-runner.", | |
| file=sys.stderr, | |
| ) | |
| return 2 | |
| harness_name = args.harness or ("structured" if demo_runner else "dspy-rlm") | |
| try: | |
| inference_harness = build_inference_harness(harness_name) | |
| readiness_check = getattr(inference_harness, "check_ready", None) | |
| if callable(readiness_check): | |
| readiness_check() | |
| except (RlmUnavailable, RuntimeError, ValueError) as exc: | |
| if harness_name == "dspy-rlm" and args.harness is None: | |
| # The default harness is unavailable on this machine. Degrade to | |
| # the structured harness with the reason stated up front, rather | |
| # than refusing to start. An operator who explicitly asked for | |
| # --harness dspy-rlm still gets the hard failure below, because an | |
| # explicit request must never be silently substituted. | |
| print( | |
| f"DSPy RLM harness unavailable: {exc}\n" | |
| "Falling back to the structured tool harness for this worker. " | |
| "Run --prepare-rlm (with Deno available) to enable the RLM harness.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| harness_name = "structured" | |
| inference_harness = build_inference_harness(harness_name) | |
| else: | |
| print(f"Inference harness unavailable: {exc}", file=sys.stderr) | |
| return 2 | |
| requested_agent_id = args.agent_id or ( | |
| "" if args.server and transport is None else _default_agent_id() | |
| ) | |
| try: | |
| capabilities = AgentCapabilities( | |
| agent_id=requested_agent_id, | |
| name=args.name, | |
| os=platform.system() or "unknown", | |
| arch=platform.machine() or "unknown", | |
| cpu=platform.processor() or "unknown", | |
| ram_gb=_ram_gb(), | |
| models=tuple(manifest.id for manifest in manifests), | |
| tools=advertised_tool_refs(), | |
| energy_provider=energy_meter.provider, | |
| # Advertised, so the person choosing a worker is choosing how | |
| # their request will be answered rather than finding out | |
| # afterwards. One mode, because a worker runs one harness: the | |
| # honest advertisement is what it will actually do, not what it | |
| # could be reconfigured to do. | |
| modes=(_advertised_mode(harness_name),), | |
| queue_capacity=args.queue_capacity, | |
| max_concurrency=1, | |
| ) | |
| except (TypeError, ValueError) as exc: | |
| print(f"Invalid agent capabilities: {exc}", file=sys.stderr) | |
| return 2 | |
| # The guard and the deny list are wired only on the real server path: a | |
| # local smoke test has no user identity to protect anyone from. The deny | |
| # list is agent-local, in memory, and never leaves this machine. | |
| deny_list = None | |
| if args.server: | |
| from .guard import DenyList | |
| deny_list = DenyList() | |
| guard_model_id = None | |
| if args.guard_model: | |
| if args.guard_model not in {manifest.id for manifest in manifests}: | |
| print( | |
| f"--guard-model {args.guard_model!r} is not among the models this " | |
| "worker offers.", | |
| file=sys.stderr, | |
| ) | |
| return 2 | |
| guard_model_id = args.guard_model | |
| # The read-only view a sandboxed request is given: the bundled llama.cpp | |
| # runtime and the interpreter's own tree, and nothing else. It is shared | |
| # and immutable, so no per-request copy is ever made. | |
| runtime_paths = [ | |
| path | |
| for path in ( | |
| Path(__file__).resolve().parent.parent / "runtime", | |
| Path(sys.prefix), | |
| ) | |
| if path.exists() | |
| ] | |
| executor = LocalInferenceExecutor( | |
| models=models, | |
| manifests=manifests, | |
| weights=weights, | |
| runner=runner, | |
| energy_meter=energy_meter, | |
| generated_execution=generated_policy, | |
| tool_harness=inference_harness, | |
| guard_enabled=bool(args.server) and not args.no_guard, | |
| guard_model_id=guard_model_id, | |
| deny_list=deny_list, | |
| request_isolation=not args.no_request_sandbox, | |
| sandbox_runtime_paths=runtime_paths, | |
| ) | |
| gate = None | |
| from distinct_protocol.handshake import Allowance | |
| # DIRECT MODE, ADDED TO WHATEVER ELSE THIS WORKER IS DOING. | |
| # | |
| # DIRECT MODE HAS BEEN REMOVED, AND THE REASON IS THE WHOLE ACCESS MODEL. | |
| # | |
| # `--direct` made the worker hold its own listening socket and answer a | |
| # browser with no server in between. It had no authentication of any kind: | |
| # a red-team pass submitted a job and read the answer back with no Hugging | |
| # Face account, no access code and no server, using nothing but curl. On | |
| # the loopback default that meant any local user on a shared machine; with | |
| # `--direct 0.0.0.0` it meant anyone on the network; behind a tunnel it | |
| # meant the internet. | |
| # | |
| # That was not a defect in the code, which said plainly what it did. It was | |
| # a second front door, built for the case where the operator and the user | |
| # are the same person, and it is incompatible with the requirement that | |
| # nobody reaches a volunteer's machine without redeeming an access code and | |
| # signing in. A worker now has exactly one way in: the server it paired | |
| # with, pulling work it agreed to accept. | |
| # | |
| # `tests/test_loopback_policy.py` no longer exempts any listener, which is | |
| # the check that stops this coming back by accident. | |
| if transport is None: | |
| if args.server: | |
| # ONE MACHINE, SEVERAL SERVERS, ONE QUEUE. | |
| # | |
| # Each server is paired and consented to separately: its own | |
| # catalogue, its own operator approval, its own credentials and its | |
| # own allowance gate. That separation is the point. Joining a | |
| # second server must not disclose to it what the first was | |
| # offered, and approving a model for one is not approving it for | |
| # the other. | |
| # | |
| # What is shared is the machine: one queue, one capacity figure, | |
| # one set of weights. See distinct_agent/fanout.py for why running | |
| # a loop per server instead would have every server believing it | |
| # had the whole machine to itself. | |
| pairs = _server_pairs(args.server, args.pair) | |
| if pairs is None: | |
| return 2 | |
| # One registry for the machine, holding a separate record, secret | |
| # and approval per server. Persisted, so a worker restarted | |
| # tomorrow still knows which servers it agreed to serve and on | |
| # what terms. | |
| server_registry = ServerRegistry( | |
| Allowance( | |
| models=tuple(manifest.id for manifest in manifests), | |
| tools=capabilities.tools, | |
| ), | |
| path=_servers_file(args.servers_file), | |
| ) | |
| # One code for this worker, generated here and given to every | |
| # server it joins. Printed once, loudly, because it is the only | |
| # time anybody sees it: the servers keep a digest and this process | |
| # does not write it anywhere. | |
| access_code = new_access_code() | |
| # PRINTED AFTER THE JOIN, NOT BEFORE IT. | |
| # | |
| # This used to be announced here, which put two high-entropy codes | |
| # on screen at once: the access code to share, and then the claim | |
| # code to enter. Two codes visible together is the same confusion | |
| # this whole change exists to remove -- the person scrolls back, | |
| # finds *a* code, and uses it for the wrong thing. | |
| # | |
| # One at a time, in the order they are needed: the claim code while | |
| # the worker waits, and the access code once it is actually running | |
| # and there is something worth sharing. | |
| # Kept so the dashboard can show it. It is printed once and stored | |
| # nowhere on disk, so a screen that could not show it would leave | |
| # somebody who scrolled past it with no way to get it back short of | |
| # restarting the worker, which issues a different code and retires | |
| # this one. | |
| args._access_code = access_code | |
| links: list[ServerLink] = [] | |
| gates: dict[str, AllowanceGate] = {} | |
| for url, code in pairs: | |
| link, gate_for = _join_server( | |
| url, | |
| code, | |
| capabilities=capabilities, | |
| manifests=manifests, | |
| client_factory=client_factory, | |
| allow_remote=args.allow_remote_server, | |
| allow_plaintext=args.allow_plaintext_server, | |
| access_code=access_code, | |
| approve_flag=args.approve, | |
| registry=server_registry, | |
| ) | |
| if link is None: | |
| # Refused, unreachable or unapproved. Named by | |
| # _join_server; the worker carries on with the rest, | |
| # because one server declining is not a reason to abandon | |
| # the others. | |
| continue | |
| links.append(link) | |
| gates[link.name] = gate_for | |
| capabilities = replace(capabilities, agent_id=link.transport.agent_id) | |
| if not links: | |
| print( | |
| "Not starting: no server was joined. Nothing is wrong with your " | |
| "machine and nothing was left running.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| return 2 | |
| # `--server` is repeatable, so this is a list. One worker can serve | |
| # several servers with the same code, and the invitation names them | |
| # all rather than picking one and quietly misdirecting the people | |
| # sent to the others. | |
| print( | |
| announce_access_code(access_code, servers=tuple(args.server or ())), | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| if len(links) == 1: | |
| # One server: the transport it paired with, unwrapped. The | |
| # fan-out is a real object with real behaviour and there is no | |
| # reason to put it in the path of the common case. | |
| transport = links[0].transport | |
| gate = gates[links[0].name] | |
| else: | |
| transport = MultiServerTransport( | |
| links, report=lambda message: print(message, file=sys.stderr, flush=True) | |
| ) | |
| gate = _CombinedGate(gates, transport) | |
| print( | |
| f"Serving {len(links)} servers from one queue:\n" | |
| + transport.describe(), | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| else: | |
| offer = None | |
| if args.demo_prompt is not None: | |
| offer = JobSpec( | |
| id=f"demo-{uuid.uuid4().hex}", | |
| session_id="demo-session", | |
| conversation_id="demo-conversation", | |
| parent_job_id=None, | |
| prompt=args.demo_prompt, | |
| model_id=model_id, | |
| allowed_tools=(), | |
| target_agent_id=capabilities.agent_id, | |
| created_at=time.time(), | |
| ) | |
| transport = ConsoleTransport(offer) | |
| else: | |
| transported_agent_id = getattr(transport, "agent_id", None) | |
| if args.agent_id and transported_agent_id and args.agent_id != transported_agent_id: | |
| print("--agent-id does not match the injected transport credential.", file=sys.stderr) | |
| return 2 | |
| if isinstance(transported_agent_id, str) and transported_agent_id: | |
| capabilities = replace(capabilities, agent_id=transported_agent_id) | |
| elif not capabilities.agent_id: | |
| print("Injected transport must expose an agent id.", file=sys.stderr) | |
| return 2 | |
| # One cadence, and the agent honours the one the server advertised, so the | |
| # rate the UI describes is the rate that actually happens. An explicit | |
| # --poll-interval still wins, for operators who need to throttle. | |
| if args.poll_interval is not None: | |
| poll_interval = args.poll_interval | |
| elif isinstance(transport, GradioAgentTransport): | |
| poll_interval = transport.poll_interval_s | |
| else: | |
| poll_interval = DEFAULT_POLL_INTERVAL_SECONDS | |
| worker = WorkerLoop( | |
| capabilities=capabilities, | |
| transport=transport, | |
| executor=executor, | |
| queue=InMemoryJobQueue( | |
| capacity=args.queue_capacity, | |
| max_active=1, | |
| agent_id=capabilities.agent_id, | |
| ), | |
| settings=WorkerLoopSettings(poll_interval_seconds=poll_interval), | |
| # The allowlist boundary. Never None on the real server path: even a | |
| # worker whose operator approved nothing gets a gate, and that gate | |
| # refuses everything, which is the designed meaning of "no approval". | |
| gate=gate, | |
| # Agent-local refusals, keyed on the server-derived pseudonymous user | |
| # key. Checked before the gate: whether this machine serves this | |
| # person is prior to what they asked for. | |
| deny_list=deny_list, | |
| ) | |
| if isinstance(transport, ConsoleTransport): | |
| worker.run_cycle() | |
| if args.demo_prompt is None: | |
| return 0 | |
| deadline = time.monotonic() + 30.0 | |
| while not transport.done.is_set() and time.monotonic() < deadline: | |
| worker.run_cycle() | |
| transport.done.wait(poll_interval) | |
| worker.shutdown() | |
| return 0 if transport.done.is_set() else 1 | |
| if args.dashboard: | |
| return _run_with_dashboard( | |
| worker, | |
| transport=transport, | |
| capabilities=capabilities, | |
| args=args, | |
| containment=isolation_report, | |
| request_sandbox=request_report, | |
| ) | |
| exit_code = 0 | |
| try: | |
| worker.run_forever() | |
| except KeyboardInterrupt: | |
| pass | |
| except AgentTransportError as exc: | |
| print(f"Agent transport stopped: {exc}", file=sys.stderr) | |
| exit_code = 1 | |
| finally: | |
| worker.shutdown() | |
| close = getattr(transport, "close", None) | |
| if callable(close): | |
| close() | |
| return exit_code | |
| def _run_with_dashboard( | |
| worker, | |
| *, | |
| transport, | |
| capabilities, | |
| args, | |
| containment, | |
| request_sandbox, | |
| ) -> int: | |
| """Run the same worker, with the screen drawing instead of a log scrolling. | |
| The worker runs on a thread and the screen draws on this one, rather than | |
| the other way round, for one reason: `Ctrl+C` is delivered to the main | |
| thread. Putting the worker there and the screen on a background thread | |
| would mean an interrupt landing inside the poll loop, which is the one | |
| place a half-finished job could be left in an unclear state. | |
| Every exit path stops the worker and closes the transport, including the | |
| path where the screen itself fails: a dashboard that crashed and left a | |
| worker serving invisibly would be worse than no dashboard. | |
| """ | |
| from .dashboard import ActivityLog, WorkerView | |
| try: | |
| from .tui import DashboardApp | |
| except ImportError as exc: # pragma: no cover - only without the dependency | |
| # A traceback about a module nobody asked for is not an answer. The | |
| # worker itself does not need Textual, so this is the one place its | |
| # absence matters and the one place worth explaining it. | |
| raise SystemExit( | |
| f"The live status screen needs the 'textual' package ({exc}). " | |
| "Install this project's dependencies with `pip install -e .` in the " | |
| "folder you cloned, or drop --dashboard to run with a scrolling log." | |
| ) from exc | |
| log = getattr(sys.stderr, "_distinct_activity_log", None) | |
| if log is None: | |
| log = ActivityLog() | |
| base = WorkerView( | |
| name=capabilities.name, | |
| agent_id=capabilities.agent_id, | |
| servers=tuple(args.server or ()), | |
| connected=True, | |
| platform_name=f"{platform.system()} {platform.release()} ({capabilities.arch})".strip(), | |
| cpu=capabilities.cpu, | |
| ram_gb=capabilities.ram_gb, | |
| models=tuple(capabilities.models), | |
| tools=tuple(capabilities.tools), | |
| queue_capacity=capabilities.queue_capacity, | |
| energy_provider=capabilities.energy_provider, | |
| containment=_report_sentence(containment), | |
| request_sandbox=_report_sentence(request_sandbox), | |
| access_code=getattr(args, "_access_code", "") or "", | |
| ) | |
| stop = threading.Event() | |
| failure: list[str] = [] | |
| def serve() -> None: | |
| try: | |
| worker.run_forever(stop_event=stop) | |
| except AgentTransportError as exc: | |
| failure.append(f"The connection stopped: {exc}") | |
| except Exception as exc: # noqa: BLE001 - reported on the screen | |
| failure.append(f"The worker stopped: {type(exc).__name__}: {exc}") | |
| finally: | |
| stop.set() | |
| thread = threading.Thread(target=serve, name="distinct-worker", daemon=True) | |
| thread.start() | |
| # From here the terminal belongs to the application, so the log stops | |
| # echoing to it and keeps the lines for the activity pane instead. | |
| log.detach() | |
| try: | |
| DashboardApp(worker=worker, base=base, activity=log, stop=stop).run() | |
| except KeyboardInterrupt: | |
| pass | |
| finally: | |
| stop.set() | |
| thread.join(timeout=10) | |
| worker.shutdown() | |
| close = getattr(transport, "close", None) | |
| if callable(close): | |
| close() | |
| # Restore the console before anything else prints, or the summary below | |
| # is written into a screen that is no longer being redrawn. | |
| if getattr(sys.stderr, "_distinct_activity_log", None) is not None: | |
| sys.stderr = sys.__stderr__ | |
| for message in failure: | |
| print(message, file=sys.stderr) | |
| return 1 if failure else 0 | |
| def _report_sentence(report) -> str: | |
| """One line from an isolation report, in the report's own words. | |
| Reads `mechanism` and `reason` rather than reformatting them, because those | |
| strings are already written to be honest about what is and is not applied, | |
| and a dashboard that paraphrased them would be a second, laxer description | |
| of the same thing. | |
| """ | |
| if report is None: | |
| return "unknown" | |
| mechanism = str(getattr(report, "mechanism", "") or "") | |
| applied = bool(getattr(report, "applied", False)) | |
| reason = str(getattr(report, "reason", "") or "") | |
| if applied and mechanism: | |
| return mechanism | |
| if mechanism and mechanism != "none": | |
| return f"{mechanism} available, not applied" | |
| return reason or "none" | |
| def _operator_approval(choice, *, approve_flag: bool) -> bool: | |
| """Decide whether the operator approves the pending choice set. | |
| ``--approve`` is an explicit gesture given on the command line by the | |
| person starting the worker; it approves the full choice set. Otherwise, a | |
| console that can ask, asks. A console that cannot ask approves nothing, | |
| because auto-accepting on a headless box is exactly the path this | |
| handshake exists to close. | |
| **Every path out of here says what happened and what to do about it.** The | |
| previous version printed the question and, when the answer was empty or the | |
| console could not be read, printed "Nothing approved" on the same line. An | |
| operator saw their prompt collide with a refusal and had no way to tell | |
| whether they had pressed the wrong key, whether the worker had failed, or | |
| what to type next. A refusal is a legitimate outcome; an unexplained one is | |
| not. | |
| """ | |
| if not choice: | |
| print( | |
| "\nNothing to approve. The server's catalogue and this worker's offer " | |
| "have no models or library members in common, so there is no set to " | |
| "agree on. Check --list-models against the server's catalogue.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| return False | |
| if approve_flag: | |
| return True | |
| if not sys.stdin.isatty(): | |
| print( | |
| "\nApproval was NOT requested, because this console cannot be read " | |
| "(the worker is running without an interactive terminal, for example " | |
| "from a service, a scheduled task, or a batch file whose input is " | |
| "redirected).\n" | |
| "Approving by default on a machine nobody is watching is exactly what " | |
| "this handshake exists to prevent, so the worker approved nothing.\n" | |
| "To run unattended, restart it with --approve, which approves the set " | |
| "below for this server:\n" | |
| f" models: {', '.join(choice.models) or 'none'}\n" | |
| f" library (tools and skills): {', '.join(choice.tools) or 'none'}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| return False | |
| print( | |
| "\nThis server may ask this worker to run:\n" | |
| f" models: {', '.join(choice.models) or 'none'}\n" | |
| f" library (tools and skills): {', '.join(choice.tools) or 'none'}\n" | |
| "Approving binds this exact set to this server's current catalogue. The " | |
| "worker refuses anything outside it, whatever the server later sends.\n" | |
| "Answer 'n', or press Enter, to approve nothing and stop.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| try: | |
| answer = input("Approve this exact set? [y/N] ") | |
| except (EOFError, KeyboardInterrupt): | |
| print( | |
| "\nNo answer was given, so nothing was approved. Start the worker " | |
| "again and answer 'y', or pass --approve.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| return False | |
| if answer.strip().lower() in {"y", "yes"}: | |
| return True | |
| print( | |
| "\nDeclined. Nothing was approved, so this worker will advertise nothing " | |
| "and refuse every job. That is a working, safe state, not an error: it is " | |
| "simply a worker that has agreed to do nothing yet.\n" | |
| "To contribute, start it again and answer 'y', or pass --approve. To offer " | |
| "a narrower set, restart with --models naming only the models you want.", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| return False | |
| def _download_reporter(model_id: str) -> Callable[[float, str], None]: | |
| """Print download progress in tenths, so a long fetch is visibly alive. | |
| Bound outside the loop it is used in: a closure over the loop variable | |
| would report every model under the last model's name. | |
| """ | |
| last = [-1] | |
| def report(fraction: float, message: str) -> None: | |
| percent = int(fraction * 100) | |
| if percent >= last[0] + 10: | |
| last[0] = percent | |
| print(f" {model_id}: {percent}%", flush=True) | |
| return report | |
| def _requested_model_ids(values: Iterable[str]) -> tuple[str, ...]: | |
| """Parse ``--models`` into an ordered, de-duplicated tuple of ids. | |
| Accepts the flag repeated and comma-separated within one value, because | |
| both read naturally and refusing one of them is an argument with the | |
| operator rather than a safety property. | |
| """ | |
| seen: list[str] = [] | |
| for value in values or (): | |
| for part in str(value).split(","): | |
| model_id = part.strip() | |
| if model_id and model_id not in seen: | |
| seen.append(model_id) | |
| return tuple(seen) | |
| def _default_agent_id() -> str: | |
| host = re.sub(r"[^A-Za-z0-9_.-]+", "-", platform.node()).strip("-.") | |
| return (host or "local-agent")[:128] | |
| def _ram_gb() -> float: | |
| try: | |
| if os.name == "nt": | |
| class MemoryStatus(ctypes.Structure): | |
| _fields_ = [ | |
| ("length", ctypes.c_ulong), | |
| ("memory_load", ctypes.c_ulong), | |
| ("total_phys", ctypes.c_ulonglong), | |
| ("avail_phys", ctypes.c_ulonglong), | |
| ("total_page_file", ctypes.c_ulonglong), | |
| ("avail_page_file", ctypes.c_ulonglong), | |
| ("total_virtual", ctypes.c_ulonglong), | |
| ("avail_virtual", ctypes.c_ulonglong), | |
| ("avail_extended_virtual", ctypes.c_ulonglong), | |
| ] | |
| status = MemoryStatus() | |
| status.length = ctypes.sizeof(MemoryStatus) | |
| if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)): | |
| return round(status.total_phys / (1024**3), 2) | |
| pages = os.sysconf("SC_PHYS_PAGES") | |
| page_size = os.sysconf("SC_PAGE_SIZE") | |
| return round((pages * page_size) / (1024**3), 2) | |
| except (AttributeError, OSError, ValueError): | |
| return 0.0 | |
| if __name__ == "__main__": # pragma: no cover | |
| raise SystemExit(main()) | |