fruxon 0.9.2__tar.gz → 0.9.4__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.
- {fruxon-0.9.2 → fruxon-0.9.4}/PKG-INFO +1 -1
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/_version.py +2 -2
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/auth.py +8 -2
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli_auth.py +20 -4
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/credentials.py +56 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/conftest.py +8 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_cli.py +3 -3
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_credentials.py +21 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/.gitignore +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/HISTORY.md +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/LICENSE +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/README.md +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/pyproject.toml +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/__init__.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/__main__.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/_ssl.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/__init__.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/_schema.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/_shared.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents_budget.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents_draft.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents_revisions.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents_tests.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/chat.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/completion.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/config.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/describe.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/doctor.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/examples.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/guides.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/integrations.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/keys.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/llm_providers.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/run.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/skills.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/tools.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/trace.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/doctor.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/exceptions.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/fruxon.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/models.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/output.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/params.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/__init__.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-build-agent/SKILL.md +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-debug-revision/SKILL.md +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-meet/SKILL.md +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/telemetry.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/ui.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/update_check.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/validation.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/__init__.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_actor.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_budgets.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_client.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_doctor.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_draft_evaluate_cli.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_drafts.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_fruxon.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_guides.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_output.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_params.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_schema.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_skills.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_ssl.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_telemetry.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_test_chats.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_ui.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_update_check.py +0 -0
- {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fruxon
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: The Fruxon SDK is a lightweight Python client for integrating with the Fruxon platform.
|
|
5
5
|
Project-URL: bugs, https://github.com/fruxon-ai/fruxon-sdk/issues
|
|
6
6
|
Project-URL: changelog, https://github.com/fruxon-ai/fruxon-sdk/blob/main/HISTORY.md
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.9.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 9,
|
|
21
|
+
__version__ = version = '0.9.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 9, 4)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -221,9 +221,15 @@ def _browser_login(
|
|
|
221
221
|
# the CLI uses (override > stored > default).
|
|
222
222
|
resolved_base = override_base_url or FruxonClient.DEFAULT_BASE_URL
|
|
223
223
|
|
|
224
|
-
# 1) Kick off the grant.
|
|
224
|
+
# 1) Kick off the grant. Pass this machine's stable device id so a
|
|
225
|
+
# re-login from here rotates the existing credential in place instead of
|
|
226
|
+
# minting a new one (and orphaning the old).
|
|
225
227
|
try:
|
|
226
|
-
grant = cli_auth.start_grant(
|
|
228
|
+
grant = cli_auth.start_grant(
|
|
229
|
+
resolved_base,
|
|
230
|
+
device_id=credentials.device_id(),
|
|
231
|
+
device_label=credentials.device_label(),
|
|
232
|
+
)
|
|
227
233
|
except Exception as exc: # noqa: BLE001 — surface anything here as a clean error
|
|
228
234
|
fail(
|
|
229
235
|
f"Couldn't start the browser sign-in: {exc}",
|
|
@@ -86,20 +86,36 @@ class PollExpired:
|
|
|
86
86
|
PollResult = PollPending | PollApproved | PollExpired
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def start_grant(
|
|
89
|
+
def start_grant(
|
|
90
|
+
base_url: str,
|
|
91
|
+
*,
|
|
92
|
+
device_id: str | None = None,
|
|
93
|
+
device_label: str | None = None,
|
|
94
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
95
|
+
) -> GrantStart:
|
|
90
96
|
"""Begin a CLI login flow. Returns the verification URL + grant code.
|
|
91
97
|
|
|
98
|
+
``device_id`` / ``device_label`` identify this machine so the backend
|
|
99
|
+
binds the minted credential to it and rotates in place on re-login
|
|
100
|
+
(see ``credentials.device_id``). Omitting them is valid — the backend
|
|
101
|
+
falls back to minting a fresh credential.
|
|
102
|
+
|
|
92
103
|
Network errors raise :class:`FruxonConnectionError`; API errors
|
|
93
104
|
(rate limit, 5xx) raise :class:`FruxonAPIError`. The CLI surfaces
|
|
94
105
|
these with their normal hint mapping.
|
|
95
106
|
"""
|
|
96
107
|
url = base_url.rstrip("/") + _PATH_START
|
|
108
|
+
# Only include keys we actually have, so an older backend that ignores
|
|
109
|
+
# them and a newer one that reads them both stay happy.
|
|
110
|
+
payload: dict[str, str] = {}
|
|
111
|
+
if device_id:
|
|
112
|
+
payload["deviceId"] = device_id
|
|
113
|
+
if device_label:
|
|
114
|
+
payload["deviceLabel"] = device_label
|
|
97
115
|
req = urllib.request.Request(
|
|
98
116
|
url,
|
|
99
117
|
method="POST",
|
|
100
|
-
|
|
101
|
-
# set so the server's request-parser doesn't 415 on us.
|
|
102
|
-
data=b"{}",
|
|
118
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
103
119
|
headers={
|
|
104
120
|
"Accept": "application/json",
|
|
105
121
|
"Content-Type": "application/json",
|
|
@@ -100,6 +100,62 @@ def credentials_path() -> Path:
|
|
|
100
100
|
return config_dir() / CREDENTIALS_FILE
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
DEVICE_ID_FILE = "device-id"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def device_id_path() -> Path:
|
|
107
|
+
return config_dir() / DEVICE_ID_FILE
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def device_id() -> str:
|
|
111
|
+
"""Stable per-install id identifying this machine to the CLI-auth flow.
|
|
112
|
+
|
|
113
|
+
Generated once and persisted under the config dir in its own file —
|
|
114
|
+
deliberately separate from the credentials file so it survives
|
|
115
|
+
``logout`` / ``clear()``. It's machine identity, not a secret: the
|
|
116
|
+
backend uses it to bind the minted CLI token to this machine, so a
|
|
117
|
+
later ``fruxon login`` from here rotates that credential in place
|
|
118
|
+
(continuous activity, no orphaned keys) rather than minting a new one.
|
|
119
|
+
"""
|
|
120
|
+
path = device_id_path()
|
|
121
|
+
try:
|
|
122
|
+
existing = path.read_text(encoding="utf-8").strip()
|
|
123
|
+
if existing:
|
|
124
|
+
return existing
|
|
125
|
+
except OSError:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
import uuid
|
|
129
|
+
|
|
130
|
+
new_id = str(uuid.uuid4())
|
|
131
|
+
try:
|
|
132
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
134
|
+
tmp.write_text(new_id + "\n", encoding="utf-8")
|
|
135
|
+
os.replace(tmp, path)
|
|
136
|
+
except OSError:
|
|
137
|
+
# Can't persist (read-only home, locked-down container) — return the
|
|
138
|
+
# fresh id anyway. This login just won't get rotation continuity;
|
|
139
|
+
# it'll mint a new credential, same as a first-ever login.
|
|
140
|
+
_logger.debug("could not persist device id", exc_info=True)
|
|
141
|
+
return new_id
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def device_label() -> str | None:
|
|
145
|
+
"""Best-effort human-readable machine label (hostname) for the dashboard.
|
|
146
|
+
|
|
147
|
+
Attribution only — never sent as anything load-bearing. Returns None if
|
|
148
|
+
the hostname can't be resolved.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
import socket
|
|
152
|
+
|
|
153
|
+
name = socket.gethostname().strip()
|
|
154
|
+
return name or None
|
|
155
|
+
except OSError:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
103
159
|
def load() -> StoredCredentials:
|
|
104
160
|
"""Read the stored credentials: keyring for the secret, file for the rest.
|
|
105
161
|
|
|
@@ -59,6 +59,14 @@ def _scrub_cli_env(monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.Tem
|
|
|
59
59
|
|
|
60
60
|
for var in _SCRUB_ENV:
|
|
61
61
|
monkeypatch.delenv(var, raising=False)
|
|
62
|
+
# Hard-disable telemetry for the whole suite. Without this, any test
|
|
63
|
+
# that drives a real CLI command flow (e.g. the doctor tests, which
|
|
64
|
+
# set FRUXON_TOKEN=fx_pat_x) spins up the background telemetry thread
|
|
65
|
+
# and POSTs to the *production* ingest endpoint — leaking bogus events
|
|
66
|
+
# under a fake token and tripping its rate limiter. Tests that assert
|
|
67
|
+
# telemetry's own on/off resolution clear this var in their local
|
|
68
|
+
# fixture (and mock the network), so they're unaffected.
|
|
69
|
+
monkeypatch.setenv("FRUXON_NO_TELEMETRY", "1")
|
|
62
70
|
monkeypatch.setenv("FRUXON_NO_KEYRING", "1")
|
|
63
71
|
config_dir = tmp_path_factory.mktemp("fruxon-config")
|
|
64
72
|
monkeypatch.setenv("FRUXON_CONFIG_DIR", str(config_dir))
|
|
@@ -221,7 +221,7 @@ class TestLoginLogoutWhoami:
|
|
|
221
221
|
# must keep polling until it sees the terminal state.
|
|
222
222
|
poll_calls = {"n": 0}
|
|
223
223
|
|
|
224
|
-
def fake_start(base_url):
|
|
224
|
+
def fake_start(base_url, **kwargs):
|
|
225
225
|
return cli_auth.GrantStart(
|
|
226
226
|
grant_code="abcdef1234567890",
|
|
227
227
|
verification_url="https://app.fruxon.com/cli-auth?code=abcdef1234567890",
|
|
@@ -265,7 +265,7 @@ class TestLoginLogoutWhoami:
|
|
|
265
265
|
monkeypatch.setattr(
|
|
266
266
|
cli_auth,
|
|
267
267
|
"start_grant",
|
|
268
|
-
lambda _base: cli_auth.GrantStart(
|
|
268
|
+
lambda _base, **_kw: cli_auth.GrantStart(
|
|
269
269
|
grant_code="x" * 32,
|
|
270
270
|
verification_url="https://app.fruxon.com/cli-auth?code=x",
|
|
271
271
|
expires_in_seconds=60,
|
|
@@ -297,7 +297,7 @@ class TestLoginLogoutWhoami:
|
|
|
297
297
|
monkeypatch.setattr(
|
|
298
298
|
cli_auth,
|
|
299
299
|
"start_grant",
|
|
300
|
-
lambda _base: cli_auth.GrantStart(
|
|
300
|
+
lambda _base, **_kw: cli_auth.GrantStart(
|
|
301
301
|
grant_code="x" * 32,
|
|
302
302
|
verification_url="https://app.fruxon.com/cli-auth?code=x",
|
|
303
303
|
expires_in_seconds=60,
|
|
@@ -306,3 +306,24 @@ class TestLoadCache:
|
|
|
306
306
|
credentials.save(credentials.StoredCredentials(token="fx_pat_x", org="acme"))
|
|
307
307
|
credentials.load()
|
|
308
308
|
assert calls["n"] == 2
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class TestDeviceId:
|
|
312
|
+
"""The per-install device id used by the CLI-auth rotate-in-place flow."""
|
|
313
|
+
|
|
314
|
+
def test_device_id_is_stable_and_persisted(self):
|
|
315
|
+
first = credentials.device_id()
|
|
316
|
+
assert first # non-empty UUID
|
|
317
|
+
# Written to its own file under the config dir.
|
|
318
|
+
assert credentials.device_id_path().is_file()
|
|
319
|
+
# Stable across calls (re-reads the same persisted value).
|
|
320
|
+
assert credentials.device_id() == first
|
|
321
|
+
|
|
322
|
+
def test_device_id_survives_logout(self):
|
|
323
|
+
# Device id is machine identity, not a credential — clearing the
|
|
324
|
+
# login (logout) must not regenerate it, or every re-login would
|
|
325
|
+
# look like a new machine and mint a fresh key.
|
|
326
|
+
did = credentials.device_id()
|
|
327
|
+
credentials.save(credentials.StoredCredentials(token="fx_pat_x", org="acme"))
|
|
328
|
+
credentials.clear()
|
|
329
|
+
assert credentials.device_id() == did
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|