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.
Files changed (74) hide show
  1. {fruxon-0.9.2 → fruxon-0.9.4}/PKG-INFO +1 -1
  2. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/_version.py +2 -2
  3. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/auth.py +8 -2
  4. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli_auth.py +20 -4
  5. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/credentials.py +56 -0
  6. {fruxon-0.9.2 → fruxon-0.9.4}/tests/conftest.py +8 -0
  7. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_cli.py +3 -3
  8. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_credentials.py +21 -0
  9. {fruxon-0.9.2 → fruxon-0.9.4}/.gitignore +0 -0
  10. {fruxon-0.9.2 → fruxon-0.9.4}/HISTORY.md +0 -0
  11. {fruxon-0.9.2 → fruxon-0.9.4}/LICENSE +0 -0
  12. {fruxon-0.9.2 → fruxon-0.9.4}/README.md +0 -0
  13. {fruxon-0.9.2 → fruxon-0.9.4}/pyproject.toml +0 -0
  14. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/__init__.py +0 -0
  15. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/__main__.py +0 -0
  16. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/_ssl.py +0 -0
  17. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/__init__.py +0 -0
  18. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/_schema.py +0 -0
  19. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/_shared.py +0 -0
  20. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents.py +0 -0
  21. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents_budget.py +0 -0
  22. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents_draft.py +0 -0
  23. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents_revisions.py +0 -0
  24. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/agents_tests.py +0 -0
  25. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/chat.py +0 -0
  26. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/completion.py +0 -0
  27. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/config.py +0 -0
  28. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/describe.py +0 -0
  29. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/doctor.py +0 -0
  30. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/examples.py +0 -0
  31. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/guides.py +0 -0
  32. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/integrations.py +0 -0
  33. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/keys.py +0 -0
  34. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/llm_providers.py +0 -0
  35. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/run.py +0 -0
  36. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/skills.py +0 -0
  37. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/tools.py +0 -0
  38. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/cli/trace.py +0 -0
  39. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/doctor.py +0 -0
  40. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/exceptions.py +0 -0
  41. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/fruxon.py +0 -0
  42. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/models.py +0 -0
  43. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/output.py +0 -0
  44. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/params.py +0 -0
  45. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/__init__.py +0 -0
  46. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
  47. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-build-agent/SKILL.md +0 -0
  48. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
  49. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-debug-revision/SKILL.md +0 -0
  50. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-meet/SKILL.md +0 -0
  51. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
  52. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/telemetry.py +0 -0
  53. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/ui.py +0 -0
  54. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/update_check.py +0 -0
  55. {fruxon-0.9.2 → fruxon-0.9.4}/src/fruxon/validation.py +0 -0
  56. {fruxon-0.9.2 → fruxon-0.9.4}/tests/__init__.py +0 -0
  57. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_actor.py +0 -0
  58. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_budgets.py +0 -0
  59. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_client.py +0 -0
  60. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_doctor.py +0 -0
  61. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_draft_evaluate_cli.py +0 -0
  62. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_drafts.py +0 -0
  63. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_fruxon.py +0 -0
  64. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_guides.py +0 -0
  65. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_output.py +0 -0
  66. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_params.py +0 -0
  67. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_schema.py +0 -0
  68. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_skills.py +0 -0
  69. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_ssl.py +0 -0
  70. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_telemetry.py +0 -0
  71. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_test_chats.py +0 -0
  72. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_ui.py +0 -0
  73. {fruxon-0.9.2 → fruxon-0.9.4}/tests/test_update_check.py +0 -0
  74. {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.2
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.2'
22
- __version_tuple__ = version_tuple = (0, 9, 2)
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(resolved_base)
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(base_url: str, *, timeout: float = DEFAULT_TIMEOUT_SECONDS) -> GrantStart:
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
- # Empty body — backend accepts an empty POST. ``Content-Type``
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