driftstack-sdk 0.2.0__tar.gz → 0.3.0__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 (38) hide show
  1. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/CHANGELOG.md +72 -0
  2. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/PKG-INFO +1 -1
  3. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/pyproject.toml +1 -1
  4. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/_generated/models.py +52 -8
  5. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/_version.py +1 -1
  6. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/errors.py +13 -0
  7. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/agent_sessions.py +3 -0
  8. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/.gitignore +0 -0
  9. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/LICENSE +0 -0
  10. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/README.md +0 -0
  11. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/__init__.py +0 -0
  12. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/_generated/__init__.py +0 -0
  13. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/client.py +0 -0
  14. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/http.py +0 -0
  15. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/pagination.py +0 -0
  16. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/py.typed +0 -0
  17. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/__init__.py +0 -0
  18. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/_common.py +0 -0
  19. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/account.py +0 -0
  20. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/api_keys.py +0 -0
  21. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/archetypes.py +0 -0
  22. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/audit_log.py +0 -0
  23. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/auth.py +0 -0
  24. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/billing.py +0 -0
  25. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/crypto_orders.py +0 -0
  26. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/egress.py +0 -0
  27. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/email_preferences.py +0 -0
  28. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/legal.py +0 -0
  29. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/mfa.py +0 -0
  30. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/profile_snapshots.py +0 -0
  31. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/profiles.py +0 -0
  32. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/recipes.py +0 -0
  33. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/sessions.py +0 -0
  34. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/team.py +0 -0
  35. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/usage.py +0 -0
  36. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/resources/webhooks.py +0 -0
  37. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/retry.py +0 -0
  38. {driftstack_sdk-0.2.0 → driftstack_sdk-0.3.0}/src/driftstack/webhook_signature.py +0 -0
@@ -6,6 +6,78 @@ follows [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] - 2026-09-22
10
+
11
+ **Nothing was removed.** Every name 0.2.0 exported, every method, every
12
+ keyword argument and every error class is still here and still means the
13
+ same thing, so upgrading takes no code change on its own — except where
14
+ **Migration** below says a string comparison needs updating. Every addition
15
+ below exists on **both** `Driftstack` and `AsyncDriftstack`.
16
+
17
+ ### Added
18
+
19
+ - **`EgressCapabilities.safeguards`** — `"passed"`, `"failed"` or
20
+ `"unverified"`, summarising whether every egress safeguard held for a
21
+ session. `"failed"` wins whenever any check did not pass; `"passed"` only
22
+ when the device declared the full set of checks a healthy session reports
23
+ and every one of them reported back; `"unverified"` otherwise. The field is
24
+ **absent**, not `None`, on a session reported before it existed — do not
25
+ read an absent value as `"unverified"` or `"passed"`. Rides everywhere
26
+ `egress_capabilities` already does: `sessions.get()` / `.list()` /
27
+ `.create()`, `profiles.launch()`, and the
28
+ `session.egress_capability_changed` webhook payload.
29
+ - **`measured_by`** on a `?check=full` proxy test result
30
+ (`egress.test_proxy(proxy_id)`) — `"phone"` when a real phone session took
31
+ the measurement, `"driftstack"` when Driftstack itself did because no phone
32
+ could be reached in time. The field this replaces, `measured_from`, is
33
+ still sent beside it with its original values for existing integrations,
34
+ but is no longer documented; read `measured_by` from here on.
35
+ - **`direct_reading` and `website_like_reading`** on `os_fingerprint` — the
36
+ same two facts `single_host_vantage` and `web_port_vantage` already carry,
37
+ under plainer names, added beside the originals rather than replacing
38
+ them. Present on the proxy test result **and** on each saved
39
+ proxy returned by `egress.list_proxies()` / `.update_proxy(proxy_id, body)`.
40
+ - **`"page_unreadable"`** joins the `AgentNoticeReason` values a
41
+ `plan-executed` result can carry: the page could not be read to plan the
42
+ next step, so the task stopped rather than guess. Send `continue` to try
43
+ again.
44
+
45
+ ### Changed
46
+
47
+ - **Two dead `egress_capabilities.warnings` codes are retired from the
48
+ documentation**: `quic_disabled_fallback_http2` and
49
+ `dns_remote_resolve_unsupported_by_proxy`. Neither has ever been sent, so
50
+ this is a documentation correction, not a behavioural change.
51
+
52
+ ### Migration
53
+
54
+ Two closed-string fields were narrowed — values removed, not added — inside
55
+ the `?check=full` proxy test result. `egress.test_proxy()` and
56
+ `egress.list_proxies()` return a plain `dict`, not a validated model, so the
57
+ affected `AccountProxyTestResult*` / `OsFingerprint` models in
58
+ `driftstack._generated.models` are typing-only for these two fields: neither
59
+ change raises at call time, and code comparing a value against one of the old
60
+ strings simply stops matching, silently. The server has sent the new values
61
+ only since 2026-09-21.
62
+
63
+ - **`not_run`** — `"node_busy"`, `"node_error"` and `"no_node"` merged into
64
+ `"check_unavailable"` (you can do exactly one thing about any of the
65
+ three: try again shortly, or contact support if it persists);
66
+ `"unresolvable"` is now `"config_unresolvable"`, matching the word the
67
+ "why a launch is refused" vocabulary already used for the identical fact.
68
+ `"live_session"` is unchanged.
69
+ - **`os_fingerprint_unavailable`** — `"vpn_tunnel"` is now
70
+ `"not_available_for_vpn"`, `"not_observed"` is now `"not_captured"`, and
71
+ `"observer_off"` is now `"not_offered_here"`.
72
+
73
+ Update any code that compares `not_run` or `os_fingerprint_unavailable`
74
+ against one of the old strings to compare against its replacement instead.
75
+
76
+ ### Pre-1.0 stability
77
+
78
+ The SDK is pre-1.0. Pin `driftstack-sdk~=0.3.0` rather than an exact version
79
+ and read this file before bumping.
80
+
9
81
  ## [0.2.0] - 2026-09-20
10
82
 
11
83
  The release the guide [Run AI tasks from your
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: driftstack-sdk
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Driftstack Python SDK — stealth iPhone Safari automation. Import as `driftstack`.
5
5
  Project-URL: Homepage, https://driftstack.io
6
6
  Project-URL: Repository, https://github.com/driftstackdev/driftstack-api
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "driftstack-sdk"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Driftstack Python SDK — stealth iPhone Safari automation. Import as `driftstack`."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: openapi.json
3
- # timestamp: 2026-09-20T08:53:40+00:00
3
+ # timestamp: 2026-09-21T22:53:06+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -149,7 +149,14 @@ class EgressCapabilities(BaseModel):
149
149
  udp_associate: bool
150
150
  quic_route: Literal["proxy", "direct", "disabled"]
151
151
  dns_remote_resolve: bool
152
+ safeguards: Literal["passed", "failed", "unverified"] | None = None
153
+ """
154
+ Whether every egress safeguard held for this session. Absent (the key is missing entirely, never `null`) on a row written before this field existed — read an absent value as unknown, never as `unverified` and never as `passed`. `passed` — every safeguard check the device reported passed, and the device declared the full set of checks a healthy session reports, so nothing was left unchecked. `failed` — at least one safeguard check did not pass. Stop relying on the session and contact support with the session id. Takes precedence over `unverified` whenever both could apply. `unverified` — we could not establish that every safeguard held: no checks were reported, a declared check never reported back, or the device declared no expected set of checks at all. Nothing is known to have failed, but completeness could not be confirmed either — for anything that matters, treat the session the same way you would `failed`.
155
+ """
152
156
  warnings: list[str] | None = []
157
+ """
158
+ Codes naming anything that did not work as asked for this session. Read them as opaque strings: a code you do not recognise is ignored rather than fatal, and a new code can appear without an SDK upgrade. Published vocabulary, with what you can do about each: `udp_unsupported_by_proxy` (the proxy refused UDP, so QUIC cannot travel through it — use a UDP-capable proxy if you need HTTP/3); `quic_unavailable` (QUIC was asked for but could not be used, and traffic fell back to HTTP/2 — retry on a new session if HTTP/3 matters); `dead_proxy` (the proxy stopped answering mid-session — check it is reachable before starting another); `streaming_blank` (the live view produced no picture; the session itself kept running — reopen the view); `streaming_failed` (the live view stopped — start a new session if you need to watch it); `safeguards_unverified` (we could not confirm every egress safeguard ran — treat the egress as unverified and start a new session if that matters); `safeguard_failed` (an egress safeguard did not pass — stop relying on the session and contact support with the session id); `safeguard_failed:direct_internet_block` (the check that nothing leaves outside your proxy did not pass — contact support with the session id); `safeguard_failed:browser_integrity` (the check that the session ran the expected browser build did not pass — contact support with the session id); `safeguard_failed:proxy_egress_verification` (the check that traffic actually left through your proxy did not pass — confirm your proxy and contact support with the session id); `safeguard_failed:live_view_capture` (the live view could not be captured; the session’s own browsing is unaffected).
159
+ """
153
160
 
154
161
 
155
162
  class Session(BaseModel):
@@ -1462,11 +1469,19 @@ class OsFingerprint(BaseModel):
1462
1469
  observed_via: Literal["proxy_host", "exit_ip"]
1463
1470
  single_host_vantage: bool
1464
1471
  """
1465
- True only when the proxy host you entered, the machine that opened the connection and the exit address are all one machine, so the reading describes the same path a website sees. Absent or false: do not draw a match/mismatch conclusion from it.
1472
+ True only when the proxy host you entered, the machine that opened the connection and the exit address are all one machine, so the reading describes the same path a website sees. Absent or false: do not draw a match/mismatch conclusion from it. Same fact as `direct_reading` below, kept under its original name for existing integrations.
1466
1473
  """
1467
1474
  web_port_vantage: bool
1468
1475
  """
1469
- True when the reading was taken on the standard HTTPS port at the proxy's IP address, with no CDN in front — the same path a website connects on. It describes what a site sees on that path; with observed_via "proxy_host" it is still a reading, not a guarantee.
1476
+ True when the reading was taken on the standard HTTPS port at the proxy's IP address, with no CDN in front — the same path a website connects on. It describes what a site sees on that path; with observed_via "proxy_host" it is still a reading, not a guarantee. Same fact as `website_like_reading` below, kept under its original name for existing integrations.
1477
+ """
1478
+ direct_reading: bool | None = None
1479
+ """
1480
+ Whether nothing sat between what was read and what your traffic actually exits from: true only when the address you gave, the address that answered, and your exit address are one machine. The customer-worded name for `single_host_vantage`.
1481
+ """
1482
+ website_like_reading: bool | None = None
1483
+ """
1484
+ Whether the reading was taken the way a real website connection is — a literal address on the standard secure-web port, not a name that could route to shared infrastructure. The customer-worded name for `web_port_vantage`.
1470
1485
  """
1471
1486
 
1472
1487
 
@@ -1551,9 +1566,16 @@ class AccountProxyTestResult1(BaseModel):
1551
1566
  os_fingerprint: OsFingerprint | None = None
1552
1567
  os_fingerprint_at: str | None = None
1553
1568
  os_fingerprint_unavailable: (
1554
- Literal["vpn_tunnel", "not_observed", "observer_off"] | None
1569
+ Literal["not_available_for_vpn", "not_captured", "not_offered_here"] | None
1555
1570
  ) = None
1571
+ """
1572
+ Why no OS fingerprint was taken, when Driftstack knows the cause: `not_available_for_vpn` (an openvpn or wireguard proxy has no single address of its own to read a stack from — no retry can help); `not_captured` (the reading was attempted and produced nothing this time — a retry may help); `not_offered_here` (this deployment does not take this reading at all — no retry can help).
1573
+ """
1556
1574
  measured_from: Literal["fleet", "control_plane"] | None = None
1575
+ measured_by: Literal["phone", "driftstack"] | None = None
1576
+ """
1577
+ Where this result was measured. `phone` — a real phone session took the measurement, the same machine `check=full` dispatches to. `driftstack` — Driftstack itself measured it: the same path `check=quick` always takes, and the honest fallback when a `check=full` request could not reach a phone in time. Present only on a `check=full` result; `check=quick` is always `driftstack` and carries no field to say so.
1578
+ """
1557
1579
 
1558
1580
 
1559
1581
  class ExitObserved1(BaseModel):
@@ -1569,7 +1591,16 @@ class AccountProxyTestResult2(BaseModel):
1569
1591
  ok: Literal[False]
1570
1592
  reason: str
1571
1593
  measured_from: Literal["fleet", "control_plane"] | None = None
1572
- not_run: Literal["live_session", "node_busy", "node_error", "no_node"] | None = None
1594
+ measured_by: Literal["phone", "driftstack"] | None = None
1595
+ """
1596
+ Where this result was measured. `phone` — a real phone session took the measurement, the same machine `check=full` dispatches to. `driftstack` — Driftstack itself measured it: the same path `check=quick` always takes, and the honest fallback when a `check=full` request could not reach a phone in time. Present only on a `check=full` result; `check=quick` is always `driftstack` and carries no field to say so.
1597
+ """
1598
+ not_run: (
1599
+ Literal["live_session", "config_unresolvable", "check_unavailable"] | None
1600
+ ) = None
1601
+ """
1602
+ Present when NOTHING RAN, so `ok:false` is not a result about the proxy: `live_session` (a full check of a VPN proxy was skipped because a live session is browsing through it — end the session to check it); `config_unresolvable` (the stored configuration could not be used, so nothing was dialled — re-add it); `check_unavailable` (the full check could not be completed on our side right now — try again shortly, or full checks are not available on this deployment).
1603
+ """
1573
1604
  exit_observed: ExitObserved1 | None = None
1574
1605
 
1575
1606
 
@@ -1585,8 +1616,17 @@ class AccountProxyTestResult3(BaseModel):
1585
1616
  ok: bool
1586
1617
  reason: str | None = None
1587
1618
  measured_from: Literal["fleet"]
1619
+ measured_by: Literal["phone"]
1620
+ """
1621
+ Where this result was measured. `phone` — a real phone session took the measurement, the same machine `check=full` dispatches to. `driftstack` — Driftstack itself measured it: the same path `check=quick` always takes, and the honest fallback when a `check=full` request could not reach a phone in time. Present only on a `check=full` result; `check=quick` is always `driftstack` and carries no field to say so.
1622
+ """
1588
1623
  node_id: str
1589
- not_run: Literal["live_session", "node_busy", "node_error", "no_node"] | None = None
1624
+ not_run: (
1625
+ Literal["live_session", "config_unresolvable", "check_unavailable"] | None
1626
+ ) = None
1627
+ """
1628
+ Present when NOTHING RAN, so `ok:false` is not a result about the proxy: `live_session` (a full check of a VPN proxy was skipped because a live session is browsing through it — end the session to check it); `config_unresolvable` (the stored configuration could not be used, so nothing was dialled — re-add it); `check_unavailable` (the full check could not be completed on our side right now — try again shortly, or full checks are not available on this deployment).
1629
+ """
1590
1630
  reachable: bool | None = None
1591
1631
  auth_ok: bool | None = None
1592
1632
  udp_associate: bool | None = None
@@ -1607,8 +1647,11 @@ class AccountProxyTestResult3(BaseModel):
1607
1647
  os_fingerprint: OsFingerprint | None = None
1608
1648
  os_fingerprint_at: str | None = None
1609
1649
  os_fingerprint_unavailable: (
1610
- Literal["vpn_tunnel", "not_observed", "observer_off"] | None
1650
+ Literal["not_available_for_vpn", "not_captured", "not_offered_here"] | None
1611
1651
  ) = None
1652
+ """
1653
+ Why no OS fingerprint was taken, when Driftstack knows the cause: `not_available_for_vpn` (an openvpn or wireguard proxy has no single address of its own to read a stack from — no retry can help); `not_captured` (the reading was attempted and produced nothing this time — a retry may help); `not_offered_here` (this deployment does not take this reading at all — no retry can help).
1654
+ """
1612
1655
 
1613
1656
 
1614
1657
  class AccountProxyTestResult(
@@ -2024,6 +2067,7 @@ class AgentMessageResponse1(BaseModel):
2024
2067
  "no_progress",
2025
2068
  "repeated_step",
2026
2069
  "ai_unavailable",
2070
+ "page_unreadable",
2027
2071
  "question",
2028
2072
  "declined",
2029
2073
  ]
@@ -2031,7 +2075,7 @@ class AgentMessageResponse1(BaseModel):
2031
2075
  | None
2032
2076
  ) = None
2033
2077
  """
2034
- Why the turn ended, in one word, whenever `notice` is present — never without it. `step_limit`: the task needs more steps than one message runs; send "continue". `time_limit`: the message was taking too long; send "continue". `budget_low`: too little of the session’s AI budget is left; start a new session. `no_progress`: the page stopped changing and the next step would repeat one that changed nothing; a person should say what to try instead. `repeated_step`: the next step would have repeated an action that already ran, which could do it twice; check the page, then send "continue" if it is safe. `ai_unavailable`: the next steps could not be worked out just now; send "continue" to try again. `question`: the agent asked you something part-way; `notice` is the question, and your answer is the next message. `declined`: the agent stopped rather than carry on; a person should decide what to do. Treat it as an OPEN string: match the values you know, and show `notice` for any other.
2078
+ Why the turn ended, in one word, whenever `notice` is present — never without it. `step_limit`: the task needs more steps than one message runs; send "continue". `time_limit`: the message was taking too long; send "continue". `budget_low`: too little of the session’s AI budget is left; start a new session. `no_progress`: the page stopped changing and the next step would repeat one that changed nothing; a person should say what to try instead. `repeated_step`: the next step would have repeated an action that already ran, which could do it twice; check the page, then send "continue" if it is safe. `ai_unavailable`: the next steps could not be worked out just now; send "continue" to try again. `page_unreadable`: the page could not be read to plan the next step; send "continue" to try again. `question`: the agent asked you something part-way; `notice` is the question, and your answer is the next message. `declined`: the agent stopped rather than carry on; a person should decide what to do. Treat it as an OPEN string: match the values you know, and show `notice` for any other.
2035
2079
  """
2036
2080
  usage: AgentMessageUsage | None = None
2037
2081
 
@@ -7,4 +7,4 @@ needed — useful for tools that scrape the version without installing.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- __version__ = "0.2.0"
10
+ __version__ = "0.3.0"
@@ -33,6 +33,19 @@ def _coerce_int(value: Any) -> int:
33
33
  return 0
34
34
 
35
35
 
36
+ def _coerce_optional_int(value: Any) -> int | None:
37
+ """Like :func:`_coerce_int`, but ``None`` stays ``None`` rather than
38
+ becoming 0 — for fields whose ABSENCE is meaningful (e.g. a 'balance'
39
+ refusal carries no ``debt_credits`` at all, which must not read as a
40
+ debt of exactly zero)."""
41
+ if value is None:
42
+ return None
43
+ try:
44
+ return int(value)
45
+ except (TypeError, ValueError):
46
+ return None
47
+
48
+
36
49
  class DriftstackError(Exception):
37
50
  """Base for every error raised by the Driftstack SDK.
38
51
 
@@ -115,6 +115,8 @@ CanonicalModifier = Literal["cmd", "ctrl", "shift", "option"]
115
115
  #: ``"continue"`` if it is safe.
116
116
  #: - ``"ai_unavailable"`` — the next steps could not be worked out just now.
117
117
  #: Send ``"continue"`` to try again.
118
+ #: - ``"page_unreadable"`` — the page could not be read to plan the next step.
119
+ #: Send ``"continue"`` to try again.
118
120
  #: - ``"question"`` — the agent asked you something part-way. ``notice`` is the
119
121
  #: question; send your answer as the next message.
120
122
  #: - ``"declined"`` — the agent stopped rather than carry on. A person should
@@ -131,6 +133,7 @@ AgentNoticeReason = (
131
133
  "no_progress",
132
134
  "repeated_step",
133
135
  "ai_unavailable",
136
+ "page_unreadable",
134
137
  "question",
135
138
  "declined",
136
139
  ]
File without changes
File without changes