hum-cli 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hum_cli-0.0.1/.github/workflows/publish.yml +24 -0
- hum_cli-0.0.1/.github/workflows/test.yml +16 -0
- hum_cli-0.0.1/PKG-INFO +24 -0
- hum_cli-0.0.1/README.md +50 -0
- hum_cli-0.0.1/hum/__init__.py +4 -0
- hum_cli-0.0.1/hum/adapters/__init__.py +0 -0
- hum_cli-0.0.1/hum/adapters/harbor.py +128 -0
- hum_cli-0.0.1/hum/auth.py +112 -0
- hum_cli-0.0.1/hum/cli.py +388 -0
- hum_cli-0.0.1/hum/runtime/__init__.py +10 -0
- hum_cli-0.0.1/hum/runtime/autonomy.py +71 -0
- hum_cli-0.0.1/hum/runtime/config.py +101 -0
- hum_cli-0.0.1/hum/runtime/drivers.py +79 -0
- hum_cli-0.0.1/hum/runtime/executor.py +120 -0
- hum_cli-0.0.1/hum/runtime/grader.py +41 -0
- hum_cli-0.0.1/hum/runtime/llm.py +225 -0
- hum_cli-0.0.1/hum/runtime/loop.py +247 -0
- hum_cli-0.0.1/hum/runtime/models.py +132 -0
- hum_cli-0.0.1/hum/runtime/observe.py +28 -0
- hum_cli-0.0.1/hum/runtime/prompt.py +9 -0
- hum_cli-0.0.1/hum/runtime/session.py +139 -0
- hum_cli-0.0.1/hum/runtime/store.py +26 -0
- hum_cli-0.0.1/hum/runtime/tools.py +284 -0
- hum_cli-0.0.1/hum/runtime/trajectory.py +68 -0
- hum_cli-0.0.1/hum/runtime/workspace.py +48 -0
- hum_cli-0.0.1/hum/sync.py +56 -0
- hum_cli-0.0.1/hum/ui.py +109 -0
- hum_cli-0.0.1/pyproject.toml +25 -0
- hum_cli-0.0.1/server/__init__.py +0 -0
- hum_cli-0.0.1/server/app.py +440 -0
- hum_cli-0.0.1/server/modal_app.py +38 -0
- hum_cli-0.0.1/tests/test_cli.py +78 -0
- hum_cli-0.0.1/tests/test_runtime.py +165 -0
- hum_cli-0.0.1/tests/test_server_and_auth.py +205 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
# Trusted publishing to PyPI (no token): tag vX.Y.Z → build → publish.
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: astral-sh/setup-uv@v5
|
|
12
|
+
- run: uv build
|
|
13
|
+
- uses: actions/upload-artifact@v4
|
|
14
|
+
with: { name: dist, path: dist/ }
|
|
15
|
+
publish:
|
|
16
|
+
needs: build
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
environment: pypi
|
|
19
|
+
permissions:
|
|
20
|
+
id-token: write
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/download-artifact@v4
|
|
23
|
+
with: { name: dist, path: dist/ }
|
|
24
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
on:
|
|
3
|
+
push: { branches: [main] }
|
|
4
|
+
pull_request:
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
strategy:
|
|
8
|
+
matrix: { os: [ubuntu-latest, macos-latest, windows-latest] }
|
|
9
|
+
runs-on: ${{ matrix.os }}
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v5
|
|
13
|
+
- run: uv python install 3.12
|
|
14
|
+
- run: uv venv --python 3.12 && uv pip install -e ".[dev,server,harbor]"
|
|
15
|
+
- run: uv run --no-project pytest -q
|
|
16
|
+
shell: bash
|
hum_cli-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hum-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: An agent harness built as an RL environment that humans can drive: session trees, interchangeable human/policy drivers, counterfactual shadow, earned autonomy, verifier in the loop.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Requires-Dist: litellm>=1.70
|
|
8
|
+
Requires-Dist: mcp>=1.2
|
|
9
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
10
|
+
Requires-Dist: pydantic>=2
|
|
11
|
+
Requires-Dist: rich>=13
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: fastapi>=0.110; extra == 'dev'
|
|
14
|
+
Requires-Dist: google-auth>=2; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: python-multipart; extra == 'dev'
|
|
18
|
+
Provides-Extra: harbor
|
|
19
|
+
Requires-Dist: harbor>=0.21; extra == 'harbor'
|
|
20
|
+
Provides-Extra: server
|
|
21
|
+
Requires-Dist: fastapi>=0.110; extra == 'server'
|
|
22
|
+
Requires-Dist: google-auth>=2; extra == 'server'
|
|
23
|
+
Requires-Dist: python-multipart; extra == 'server'
|
|
24
|
+
Requires-Dist: uvicorn[standard]; extra == 'server'
|
hum_cli-0.0.1/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Hum — हम
|
|
2
|
+
|
|
3
|
+
*We.* Human and model, one harness.
|
|
4
|
+
|
|
5
|
+
Hum is an agent harness built as an RL environment that humans can drive. The same loop serves a person at a keyboard, a policy in a rollout, or a shadow continuing from a fork — and the loop remembers who did what.
|
|
6
|
+
|
|
7
|
+
What makes it different from a coding agent:
|
|
8
|
+
|
|
9
|
+
- **Interchangeable drivers.** A human turn is a first-class move in the session tree, not a prompt.
|
|
10
|
+
- **Counterfactual shadow.** When a human overrides a proposal, the pre-intervention state is forked, the policy finishes in shadow, and the grader scores both futures. Every override becomes a preference pair whose preference is a verifier, not a rater.
|
|
11
|
+
- **Earned autonomy.** How much the policy may do unsupervised on a class of tasks is a number the world assigns from graded outcomes, updated online — `observe → propose → review → act`.
|
|
12
|
+
- **Verifier in the loop.** The grader is both a tool the agent calls and the reward the trainer reads.
|
|
13
|
+
- **Trajectory-native.** Every session is a valid ATIF trajectory with authorship, interventions, shadow branches, verdicts and token custody — a training sample as it is written.
|
|
14
|
+
- **Model-agnostic, sovereign.** Any OpenAI-compatible endpoint: frontier, open weights, or your own policy served from the trainer. Runs on Modal or on your iron.
|
|
15
|
+
|
|
16
|
+
## Run it
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install git+https://github.com/metaphi-labs/hum-cli
|
|
20
|
+
hum login # browser opens → sign in with Google → back to the terminal
|
|
21
|
+
cd your-repo
|
|
22
|
+
hum # say what we're doing; it works, streams, shows its tool calls
|
|
23
|
+
hum "make the nightly job idempotent"
|
|
24
|
+
hum resume # pick up the last session
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
While it works, just type: a message steers it, `!cmd` runs a command yourself, Ctrl-C stops the current turn. Edit files in your editor whenever you like. `/help` in a session lists the rest (`/model`, `/tools`, `/cost`).
|
|
28
|
+
|
|
29
|
+
Signing in gives you a key on Metaphi's model proxy — GLM-5.3 by default. Bringing your own model is allowed (`hum model anthropic/claude-fable-5`, `hum key anthropic <key>`); those sessions are marked as running on your model.
|
|
30
|
+
|
|
31
|
+
Other commands: `hum run "task"` (unattended, then exit — the mode rollouts use), `hum sessions`, `hum show <id>`, `hum sync`, `hum whoami`, `hum logout`.
|
|
32
|
+
|
|
33
|
+
## Deployment (Metaphi)
|
|
34
|
+
|
|
35
|
+
One Modal app, `server/modal_app.py`: sign-in (Firebase Auth in the browser, ID token verified server-side; allowlist), the **model proxy** every client talks to (OpenAI-compatible `/v1/chat/completions`; the person's Hum token is their key; every call metered per person with a monthly budget), and session intake. State is SQLite + session files on the volume `hum-data`. Routes are named — `glm-5.3` on zero-data-retention providers, `policy` = our weights from the trainer, `fable` — so moving the fleet onto our model is one route change, no client update.
|
|
36
|
+
|
|
37
|
+
`~/.hum/config.toml` on a machine can override the model, mount MCP servers (`control`, `run-unit`) as native tools, and turn on backend behaviour — grader, autonomy-gated sessions, fork-and-grade on intervention. Defaults are "be a great agent".
|
|
38
|
+
|
|
39
|
+
## As a Harbor agent
|
|
40
|
+
|
|
41
|
+
`harbor run -a hum.adapters.harbor:HumAgent -m openrouter/z-ai/glm-5.3 ...` — Terminal-Bench, MainframeBench through simhub `task_run --harness hum`, and Slime rollouts run the same class, with no human driver.
|
|
42
|
+
|
|
43
|
+
## Layout
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
hum/runtime/ session tree · drivers · loop (shadow) · autonomy · tools (+MCP) · grader · llm · observe · trajectory (ATIF)
|
|
47
|
+
hum/adapters/ harbor
|
|
48
|
+
hum/cli.py terminal client
|
|
49
|
+
deploy/ litellm proxy config
|
|
50
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Harbor adapter: the runtime as a Harbor ``BaseAgent``.
|
|
2
|
+
|
|
3
|
+
This is how the harness enters every arena that already speaks Harbor —
|
|
4
|
+
Terminal-Bench, MainframeBench through simhub's task_run, and Slime rollouts
|
|
5
|
+
(the policy is served at an OpenAI-compatible endpoint; Harbor passes it as
|
|
6
|
+
``-m openai/policy`` with ``OPENAI_BASE_URL``). Same class, no human driver.
|
|
7
|
+
|
|
8
|
+
Register with ``harbor run --agent-import-path hum.adapters.harbor:HumAgent``.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import json
|
|
14
|
+
import shlex
|
|
15
|
+
import uuid
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from harbor.agents.base import BaseAgent
|
|
20
|
+
from harbor.agents.model_connection import ModelConnectionSpec
|
|
21
|
+
from harbor.environments.base import BaseEnvironment
|
|
22
|
+
from harbor.models.agent.context import AgentContext
|
|
23
|
+
from harbor.models.trajectories import Trajectory
|
|
24
|
+
|
|
25
|
+
from hum import __version__
|
|
26
|
+
from hum.runtime import LiteLLMClient, PolicyDriver, RunConfig, Runner, Session, ToolRegistry, builtin_tools
|
|
27
|
+
from hum.runtime.executor import ExecResult, decode
|
|
28
|
+
from hum.runtime.prompt import system_prompt
|
|
29
|
+
from hum.runtime.tools import MCPToolset
|
|
30
|
+
from hum.runtime.trajectory import to_atif
|
|
31
|
+
|
|
32
|
+
AGENT_NAME = "hum"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HarborExecutor:
|
|
36
|
+
"""Executor over a Harbor environment. Files move as base64 through exec so
|
|
37
|
+
no host-side temp paths are involved; snapshots are tarballs in the sandbox."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, env: BaseEnvironment, cwd: str = "/app"):
|
|
40
|
+
self.env, self.cwd = env, cwd
|
|
41
|
+
|
|
42
|
+
async def exec(self, command: str, cwd: str | None = None, timeout_sec: int | None = None) -> ExecResult:
|
|
43
|
+
r = await self.env.exec(command, cwd=cwd or self.cwd, timeout_sec=timeout_sec)
|
|
44
|
+
return ExecResult(stdout=r.stdout or "", stderr=r.stderr or "", returncode=r.return_code if r.return_code is not None else 0)
|
|
45
|
+
|
|
46
|
+
async def read(self, path: str) -> bytes:
|
|
47
|
+
r = await self.env.exec(f"base64 -w0 {shlex.quote(path)}", cwd=self.cwd, timeout_sec=60)
|
|
48
|
+
if r.return_code not in (0, None):
|
|
49
|
+
raise FileNotFoundError(r.stderr or path)
|
|
50
|
+
return base64.b64decode(r.stdout or "")
|
|
51
|
+
|
|
52
|
+
async def write(self, path: str, data: bytes) -> None:
|
|
53
|
+
b64 = base64.b64encode(data).decode()
|
|
54
|
+
r = await self.env.exec(f"mkdir -p $(dirname {shlex.quote(path)}) && echo {b64} | base64 -d > {shlex.quote(path)}", cwd=self.cwd, timeout_sec=60)
|
|
55
|
+
if r.return_code not in (0, None):
|
|
56
|
+
raise OSError(r.stderr or f"write failed: {path}")
|
|
57
|
+
|
|
58
|
+
async def snapshot(self) -> str:
|
|
59
|
+
sid = uuid.uuid4().hex[:12]
|
|
60
|
+
await self.env.exec(f"mkdir -p /tmp/harness_snap && tar czf /tmp/harness_snap/{sid}.tgz -C {shlex.quote(self.cwd)} .", timeout_sec=600)
|
|
61
|
+
return sid
|
|
62
|
+
|
|
63
|
+
async def fork(self, snapshot_id: str):
|
|
64
|
+
# One sandbox per trial: shadow forks need a second sandbox, which Harbor
|
|
65
|
+
# does not hand out. Unattended runs never fork; attended runs use the
|
|
66
|
+
# hosted-session executor instead.
|
|
67
|
+
raise NotImplementedError("HarborExecutor cannot fork; run attended sessions on a forkable executor")
|
|
68
|
+
|
|
69
|
+
async def close(self) -> None:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class HumAgent(BaseAgent):
|
|
74
|
+
SUPPORTS_ATIF = True
|
|
75
|
+
MODEL_CONNECTION = ModelConnectionSpec() # provider inferred from the model name (openai/..., anthropic/..., openrouter/...)
|
|
76
|
+
|
|
77
|
+
def __init__(self, logs_dir: Path, model_name: str | None = None, *args: Any, temperature: float | None = None,
|
|
78
|
+
reasoning_effort: str | None = None, max_turns: int = 200, max_wall_s: float = 3600.0,
|
|
79
|
+
observation_max_bytes: int = 16_000, **kwargs: Any):
|
|
80
|
+
super().__init__(logs_dir, model_name, *args, **kwargs)
|
|
81
|
+
self._temperature = temperature
|
|
82
|
+
self._reasoning_effort = reasoning_effort
|
|
83
|
+
self._cfg = RunConfig(max_turns=int(max_turns), max_wall_s=float(max_wall_s), shadow=False)
|
|
84
|
+
self._obs_bytes = int(observation_max_bytes)
|
|
85
|
+
self._toolsets: list[MCPToolset] = []
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def name() -> str:
|
|
89
|
+
return AGENT_NAME
|
|
90
|
+
|
|
91
|
+
def version(self) -> str | None:
|
|
92
|
+
return __version__
|
|
93
|
+
|
|
94
|
+
async def setup(self, environment: BaseEnvironment) -> None:
|
|
95
|
+
return None # nothing is installed in the sandbox; the loop runs host-side
|
|
96
|
+
|
|
97
|
+
async def run(self, instruction: str, environment: BaseEnvironment, context: AgentContext) -> None:
|
|
98
|
+
executor = HarborExecutor(environment)
|
|
99
|
+
tools = ToolRegistry(observation_max_bytes=self._obs_bytes).extend(builtin_tools())
|
|
100
|
+
notes = []
|
|
101
|
+
for s in self.mcp_servers or []:
|
|
102
|
+
ts = MCPToolset(s.name, url=s.url, command=s.command, args=s.args) if s.transport == "stdio" or s.url else None
|
|
103
|
+
if ts is None:
|
|
104
|
+
continue
|
|
105
|
+
mounted = await ts.connect()
|
|
106
|
+
tools.extend(mounted)
|
|
107
|
+
self._toolsets.append(ts)
|
|
108
|
+
notes.append(f"- {s.name}: " + ", ".join(t.name for t in mounted))
|
|
109
|
+
conn = self.model_connection
|
|
110
|
+
llm = LiteLLMClient(self.model_name or "", api_base=conn.base_url, api_key=conn.api_key,
|
|
111
|
+
temperature=self._temperature, reasoning_effort=self._reasoning_effort)
|
|
112
|
+
prompt = system_prompt(tools_note="\n".join(notes))
|
|
113
|
+
session = Session({"instruction": instruction}, session_id=self.session_id, path=self.logs_dir / "session.jsonl")
|
|
114
|
+
runner = Runner(session, executor, tools, PolicyDriver(llm), config=self._cfg, system_prompt=prompt)
|
|
115
|
+
try:
|
|
116
|
+
result = await runner.run()
|
|
117
|
+
finally:
|
|
118
|
+
for ts in self._toolsets:
|
|
119
|
+
await ts.close()
|
|
120
|
+
traj = to_atif(session, agent_name=AGENT_NAME, agent_version=__version__ or "0", model_name=self.model_name,
|
|
121
|
+
system_prompt=prompt, tool_definitions=tools.to_openai())
|
|
122
|
+
Trajectory.model_validate(traj) # stay a valid ATIF document
|
|
123
|
+
(self.logs_dir / "trajectory.json").write_text(json.dumps(traj, ensure_ascii=False, indent=1))
|
|
124
|
+
context.n_input_tokens = result.usage.input_tokens
|
|
125
|
+
context.n_output_tokens = result.usage.output_tokens
|
|
126
|
+
context.n_cache_tokens = result.usage.cache_tokens
|
|
127
|
+
context.cost_usd = result.usage.cost_usd
|
|
128
|
+
context.metadata = {"harness": AGENT_NAME, "turns": result.turns, "stop_reason": result.stop_reason}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Sign-in from the terminal: ``hum login`` opens the browser (device-code flow,
|
|
2
|
+
RFC 8628), the person signs in with Google, the terminal receives a Hum token
|
|
3
|
+
plus their key on our model proxy. Credentials live in ~/.hum/credentials.json
|
|
4
|
+
(mode 0600). BYO provider keys live next to them.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import webbrowser
|
|
12
|
+
from dataclasses import asdict, dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from hum import __version__
|
|
17
|
+
from hum.runtime.store import home
|
|
18
|
+
|
|
19
|
+
DEFAULT_AUTH_URL = os.environ.get("HUM_AUTH_URL", "https://metaphi-ai--hum-web.modal.run")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Credentials:
|
|
24
|
+
auth_url: str = DEFAULT_AUTH_URL
|
|
25
|
+
access_token: str | None = None
|
|
26
|
+
email: str | None = None
|
|
27
|
+
name: str | None = None
|
|
28
|
+
api_base: str | None = None # our proxy
|
|
29
|
+
api_key: str | None = None # this person's key on it
|
|
30
|
+
default_model: str | None = None
|
|
31
|
+
byo: dict[str, str] = field(default_factory=dict) # provider -> key, user-supplied
|
|
32
|
+
model: str | None = None # user's chosen model, if any
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def logged_in(self) -> bool:
|
|
36
|
+
return bool(self.access_token and self.email)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def path() -> Path:
|
|
40
|
+
return home() / "credentials.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load() -> Credentials:
|
|
44
|
+
p = path()
|
|
45
|
+
if not p.exists():
|
|
46
|
+
return Credentials()
|
|
47
|
+
try:
|
|
48
|
+
d = json.loads(p.read_text())
|
|
49
|
+
return Credentials(**{k: v for k, v in d.items() if k in Credentials.__dataclass_fields__})
|
|
50
|
+
except Exception: # noqa: BLE001
|
|
51
|
+
return Credentials()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save(c: Credentials) -> None:
|
|
55
|
+
p = path()
|
|
56
|
+
p.write_text(json.dumps(asdict(c), indent=1))
|
|
57
|
+
os.chmod(p, 0o600)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
PROVIDER_ENV = {"openrouter": "OPENROUTER_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "openai": "OPENAI_API_KEY", "gemini": "GEMINI_API_KEY"}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def provider_of(model: str) -> str:
|
|
64
|
+
return model.split("/", 1)[0] if "/" in model else "openai"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def login(auth_url: str | None = None, open_browser: bool = True, poll_timeout_s: float = 600.0, client: Any = None) -> Credentials:
|
|
68
|
+
"""Device-code flow. ``client`` is an httpx.Client (tests inject one)."""
|
|
69
|
+
import httpx
|
|
70
|
+
|
|
71
|
+
base = (auth_url or load().auth_url or DEFAULT_AUTH_URL).rstrip("/")
|
|
72
|
+
http = client or httpx.Client(timeout=20.0)
|
|
73
|
+
r = http.post(f"{base}/device/code", json={"client": "hum-cli", "version": __version__})
|
|
74
|
+
r.raise_for_status()
|
|
75
|
+
d = r.json()
|
|
76
|
+
uri = d.get("verification_uri_complete") or d["verification_uri"]
|
|
77
|
+
print(f"\nOpen {uri}\nand confirm the code {d['user_code']}\n")
|
|
78
|
+
if open_browser:
|
|
79
|
+
try:
|
|
80
|
+
webbrowser.open(uri)
|
|
81
|
+
except Exception: # noqa: BLE001
|
|
82
|
+
pass
|
|
83
|
+
interval = float(d.get("interval", 3))
|
|
84
|
+
deadline = time.monotonic() + min(poll_timeout_s, float(d.get("expires_in", 600)))
|
|
85
|
+
while time.monotonic() < deadline:
|
|
86
|
+
time.sleep(interval)
|
|
87
|
+
t = http.post(f"{base}/device/token", json={"device_code": d["device_code"]})
|
|
88
|
+
body = t.json()
|
|
89
|
+
if t.status_code == 200 and body.get("access_token"):
|
|
90
|
+
c = load()
|
|
91
|
+
c.auth_url = base
|
|
92
|
+
c.access_token = body["access_token"]
|
|
93
|
+
c.email = body["user"]["email"]
|
|
94
|
+
c.name = body["user"].get("name")
|
|
95
|
+
m = body.get("model") or {}
|
|
96
|
+
c.api_base, c.api_key, c.default_model = m.get("api_base"), m.get("api_key"), m.get("default_model")
|
|
97
|
+
save(c)
|
|
98
|
+
return c
|
|
99
|
+
err = body.get("error")
|
|
100
|
+
if err == "slow_down":
|
|
101
|
+
interval += 2
|
|
102
|
+
elif err in ("authorization_pending", None):
|
|
103
|
+
continue
|
|
104
|
+
else:
|
|
105
|
+
raise RuntimeError(f"login failed: {err}")
|
|
106
|
+
raise TimeoutError("login timed out; run `hum login` again")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def logout() -> None:
|
|
110
|
+
c = load()
|
|
111
|
+
c.access_token = c.email = c.name = c.api_base = c.api_key = c.default_model = None
|
|
112
|
+
save(c)
|