getstack 2.0.0__tar.gz → 4.0.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 (103) hide show
  1. {getstack-2.0.0 → getstack-4.0.0}/.gitignore +11 -0
  2. getstack-4.0.0/CLAUDE.md +50 -0
  3. {getstack-2.0.0 → getstack-4.0.0}/PKG-INFO +43 -9
  4. {getstack-2.0.0 → getstack-4.0.0}/README.md +38 -8
  5. {getstack-2.0.0 → getstack-4.0.0}/pyproject.toml +34 -1
  6. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/__init__.py +203 -170
  7. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/__init__.py +1 -1
  8. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/agents.py +13 -11
  9. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/audit.py +4 -4
  10. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/authority.py +94 -109
  11. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/credentials.py +1 -3
  12. getstack-4.0.0/src/getstack/_async/delivery_methods.py +95 -0
  13. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/detector_config.py +9 -8
  14. getstack-4.0.0/src/getstack/_async/dropoffs.py +81 -0
  15. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/evidence_packs.py +6 -8
  16. getstack-4.0.0/src/getstack/_async/fleets.py +155 -0
  17. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/identity.py +30 -19
  18. getstack-4.0.0/src/getstack/_async/inbound_webhooks.py +60 -0
  19. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/intents.py +34 -24
  20. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/llm.py +35 -19
  21. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/missions.py +49 -21
  22. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/notifications.py +14 -10
  23. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/partner_connect.py +10 -10
  24. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/partner_tenants.py +49 -27
  25. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/passport_session.py +15 -8
  26. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/passports.py +217 -103
  27. getstack-4.0.0/src/getstack/_async/pii_config.py +26 -0
  28. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/proxy.py +14 -10
  29. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/reviews.py +1 -1
  30. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/scan.py +10 -7
  31. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/security_events.py +5 -7
  32. getstack-4.0.0/src/getstack/_async/services.py +78 -0
  33. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/skills.py +29 -25
  34. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/team.py +5 -5
  35. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_async/usage.py +2 -2
  36. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_authority_request.py +1 -2
  37. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/_fleet_key.py +6 -3
  38. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/agent_auth.py +42 -33
  39. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/agents.py +6 -6
  40. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/audit.py +4 -4
  41. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/auth.py +6 -10
  42. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/authority.py +57 -82
  43. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/browser_bootstrap.py +38 -28
  44. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/client.py +18 -10
  45. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/credentials.py +0 -1
  46. getstack-4.0.0/src/getstack/delivery_methods.py +92 -0
  47. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/detector_config.py +15 -18
  48. getstack-4.0.0/src/getstack/dropoffs.py +86 -0
  49. getstack-4.0.0/src/getstack/errors.py +143 -0
  50. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/evidence_packs.py +5 -8
  51. getstack-4.0.0/src/getstack/fleets.py +149 -0
  52. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/identity.py +28 -20
  53. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/inbound_webhooks.py +26 -13
  54. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/intents.py +48 -37
  55. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/llm.py +37 -20
  56. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/missions.py +95 -34
  57. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/notifications.py +13 -9
  58. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/partner_connect.py +9 -11
  59. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/partner_tenants.py +51 -27
  60. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/passport_header_verify.py +4 -7
  61. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/passport_session.py +20 -10
  62. getstack-4.0.0/src/getstack/passports.py +749 -0
  63. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/pii_config.py +7 -19
  64. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/proxy.py +20 -19
  65. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/reviews.py +11 -10
  66. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/scan.py +10 -7
  67. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/security_events.py +4 -4
  68. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/services.py +25 -27
  69. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/sign_claim.py +2 -9
  70. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/skills.py +27 -19
  71. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/team.py +5 -5
  72. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/types.py +4 -2
  73. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/usage.py +2 -2
  74. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/verify_offline.py +7 -3
  75. {getstack-2.0.0 → getstack-4.0.0}/tests/test_authority.py +86 -66
  76. {getstack-2.0.0 → getstack-4.0.0}/tests/test_delivery_methods.py +2 -2
  77. getstack-4.0.0/tests/test_dropoffs.py +119 -0
  78. {getstack-2.0.0 → getstack-4.0.0}/tests/test_errors.py +1 -1
  79. {getstack-2.0.0 → getstack-4.0.0}/tests/test_fleets.py +66 -18
  80. {getstack-2.0.0 → getstack-4.0.0}/tests/test_identity_requirements.py +3 -4
  81. {getstack-2.0.0 → getstack-4.0.0}/tests/test_inbound_webhooks.py +7 -5
  82. getstack-4.0.0/tests/test_mission_binding.py +378 -0
  83. {getstack-2.0.0 → getstack-4.0.0}/tests/test_missions.py +2 -2
  84. {getstack-2.0.0 → getstack-4.0.0}/tests/test_parity.py +119 -33
  85. {getstack-2.0.0 → getstack-4.0.0}/tests/test_partner_signing.py +13 -5
  86. {getstack-2.0.0 → getstack-4.0.0}/tests/test_sign_claim.py +1 -1
  87. getstack-2.0.0/CLAUDE.md +0 -77
  88. getstack-2.0.0/src/getstack/_async/delivery_methods.py +0 -85
  89. getstack-2.0.0/src/getstack/_async/dropoffs.py +0 -64
  90. getstack-2.0.0/src/getstack/_async/fleets.py +0 -82
  91. getstack-2.0.0/src/getstack/_async/inbound_webhooks.py +0 -47
  92. getstack-2.0.0/src/getstack/_async/pii_config.py +0 -38
  93. getstack-2.0.0/src/getstack/_async/services.py +0 -80
  94. getstack-2.0.0/src/getstack/delivery_methods.py +0 -85
  95. getstack-2.0.0/src/getstack/dropoffs.py +0 -59
  96. getstack-2.0.0/src/getstack/errors.py +0 -79
  97. getstack-2.0.0/src/getstack/fleets.py +0 -82
  98. getstack-2.0.0/src/getstack/passports.py +0 -502
  99. getstack-2.0.0/tests/test_dropoffs.py +0 -70
  100. {getstack-2.0.0 → getstack-4.0.0}/LICENSE +0 -0
  101. {getstack-2.0.0 → getstack-4.0.0}/scripts/verify_sign_claim_crosslang.py +0 -0
  102. {getstack-2.0.0 → getstack-4.0.0}/src/getstack/py.typed +0 -0
  103. {getstack-2.0.0 → getstack-4.0.0}/tests/test_evidence_packs.py +0 -0
@@ -92,3 +92,14 @@ scratchpad/
92
92
 
93
93
  # Isolated test-only LangChain, CrewAI and AutoGen verification environment.
94
94
  .tmp/fleet-framework-venv/
95
+
96
+ # Local verification output (benchmark, fleet and release evidence). Never commit.
97
+ /artifacts/
98
+
99
+ # Cleanup 2026-10: local-only files
100
+ .claude/settings.local.json
101
+ .tmp/
102
+ *.paint
103
+ *.zip
104
+ STATUS.md
105
+ design-qa.md
@@ -0,0 +1,50 @@
1
+ # getstack (Python SDK)
2
+
3
+ The Python SDK, published to PyPI as `getstack` (MIT, Python 3.9+, built with hatchling from `pyproject.toml`). It is not a pnpm workspace member and has its own tooling. Runtime dependencies: `httpx`, `cryptography`, `pyjwt`, `cbor2`. It tracks the TypeScript SDK (`packages/sdk`) service for service.
4
+
5
+ ## Structure
6
+
7
+ - `sdk-python/src/getstack/__init__.py`: the sync `Stack` and async `AsyncStack` clients, `__all__`, auth selection.
8
+ - Sync services, one module per REST domain in `sdk-python/src/getstack/` (`agents`, `passports`, `missions`, `fleets`, `authority`, `evidence_packs`, `llm`, `proxy`, `team`, `delivery_methods`, `detector_config`, `pii_config`, `partner_tenants`, `partner_connect`, and others).
9
+ - Async mirrors of the same services in `sdk-python/src/getstack/_async/`.
10
+ - `sdk-python/src/getstack/client.py`, `auth.py`, `agent_auth.py`, `credentials.py`, `browser_bootstrap.py`: HTTP clients and credential sources.
11
+ - `sdk-python/src/getstack/verify_offline.py`, `passport_header_verify.py`, `sign_claim.py`: offline Passport verification and customer-managed claim signing.
12
+ - `sdk-python/src/getstack/errors.py`: `StackError` with `NotFoundError`, `UnauthorizedError`, `ForbiddenError`, `ValidationError`, `PassportBlockedError`, `MissionEndedError`.
13
+
14
+ ## Sync and async parity
15
+
16
+ Every service and method on `Stack` must also exist on `AsyncStack` (the async modules live under `sdk-python/src/getstack/_async/`). `sdk-python/tests/test_parity.py` is the guard: it asserts both clients expose the services in `EXPECTED_SERVICES` and the integration-critical methods in `EXPECTED_METHODS`, and that signatures such as `agents.register` match. When adding a service or method, add it to both clients and to those lists. Mirror new REST routes in `packages/sdk` too.
17
+
18
+ ## Auth and offline crypto
19
+
20
+ - Credential resolution (`Stack.__init__`): a pre-built `_auth`, then `agent_id` (agent keypair mode, `AgentKeypairAuth`), then `api_key` or `STACK_API_KEY`, then `~/.stack/credentials.json` (OAuth refresh token written by the CLI), then an interactive browser bootstrap when the process has a TTY and is not CI. Otherwise a `ValueError` is raised.
21
+ - `verify_passport_offline` (`verify_offline.py`) mirrors the JS function: EdDSA via PyJWT, with `jwks_url` or `public_jwk`. It cannot see revocation.
22
+ - `sign_claim_envelope` (`sign_claim.py`) produces COSE_Sign1 for `customer_managed` agents and must stay byte-compatible with the TS SDK and the server verifier. `sdk-python/scripts/verify_sign_claim_crosslang.py` proves it by signing in Python and verifying with the JS code; run it from the repo root (needs the JS SDK's `node_modules` for `cbor-x`).
23
+
24
+ ## Mission context manager
25
+
26
+ `stack.passports.mission(agent_id=..., intent=..., services=[...])` (and `async with` on `AsyncStack`) opens a Mission and its Passport in one call, submits checkpoints on a timer, checks out on exit, then completes the Mission. If the block raises, the Mission is revoked instead. Pass `mission_id="ms_..."` to bind an existing Mission, which is then left open on exit. `continuous_mission` rotates Passports under one Mission and raises `MissionEndedError` when the Mission ends. The TypeScript SDK has no equivalent context manager; keep keyword names aligned if one is added.
27
+
28
+ ## Run and test
29
+
30
+ ```bash
31
+ cd sdk-python
32
+ python -m pip install -e ".[dev]" # pytest, ruff, mypy
33
+ ruff check . # lint, import order (rules in pyproject.toml)
34
+ ruff format --check . # formatting, line length 120
35
+ mypy src # strict typing of src/ only; tests are not type-checked
36
+ python -m pytest -q # same tests as `pnpm verify:python`
37
+ ```
38
+
39
+ Run the three checks with a Python 3.10+ interpreter (current mypy no longer runs on 3.9). Fix findings in the code; the only suppressions are narrow `# type: ignore[code]` comments in tests. The HTTP client returns untyped JSON (`Any`), so service methods wrap it in `cast("<return type>", ...)` to satisfy `warn_return_any`. A service class that defines a `list` method must annotate later signatures as `builtins.list[...]`.
40
+
41
+ The integration-guide smoke test (`scripts/smoke-test-integration-guides.py`, with `scripts/smoke-test-integration-guides-setup.mjs`) needs a local API (port 3001); `scripts/release.mjs` runs it during `/publish sdk-python` when the API is reachable and aborts the release on failure.
42
+
43
+ ## Publishing
44
+
45
+ A change to this package must be released in the same session: bump `version` in `pyproject.toml` and run `/publish sdk-python <bump>`. That runs `scripts/release.mjs`, tags `sdk-python-v*`, and `publish-sdk-python.yml` uploads to PyPI. Do not run `twine` by hand. Versions are independent of the TypeScript SDK.
46
+
47
+ ## Gotchas
48
+
49
+ - The grant field on the wire is `service_connection_id`. Notification rules use `/v1/notifications/channels` (`notifications`); delivery methods use `/v1/notifications/destinations` (`delivery_methods`).
50
+ - The monorepo's JS lockfile does not resolve Python dependencies.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: getstack
3
- Version: 2.0.0
3
+ Version: 4.0.0
4
4
  Summary: Python SDK for STACK — trust infrastructure for AI agents
5
5
  Project-URL: Homepage, https://getstack.run
6
6
  Project-URL: Documentation, https://getstack.run/docs/sdk
@@ -23,6 +23,10 @@ Requires-Dist: cbor2>=5.6.0
23
23
  Requires-Dist: cryptography>=42.0.0
24
24
  Requires-Dist: httpx>=0.25.0
25
25
  Requires-Dist: pyjwt>=2.8.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.13.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.9.0; extra == 'dev'
26
30
  Description-Content-Type: text/markdown
27
31
 
28
32
  # getstack
@@ -54,13 +58,24 @@ def run_fleet_mission_example(stack, request, provider_url="https://slack.com/ap
54
58
  repeated = stack.missions.start(request)
55
59
  assert repeated["mission_id"] == started["mission_id"]
56
60
  operation_id = hashlib.sha256(json.dumps([started["mission_id"], "support-update"]).encode()).hexdigest()
57
- response = stack.proxy.request("slack", provider_url, "POST",
61
+ response = stack.proxy.request(
62
+ "slack",
63
+ provider_url,
64
+ "POST",
58
65
  body={"channel": "C123", "text": "Support update"},
59
- passport_token=started["passport"]["token"], authority_request_id=operation_id)
66
+ passport_token=started["passport"]["token"],
67
+ authority_request_id=operation_id,
68
+ )
60
69
  assert response.status == 200
61
70
  try:
62
- stack.proxy.request("slack", provider_url, "POST", body={"channel": "OTHER", "text": "Support update"},
63
- passport_token=started["passport"]["token"], authority_request_id=operation_id + "_denied")
71
+ stack.proxy.request(
72
+ "slack",
73
+ provider_url,
74
+ "POST",
75
+ body={"channel": "OTHER", "text": "Support update"},
76
+ passport_token=started["passport"]["token"],
77
+ authority_request_id=operation_id + "_denied",
78
+ )
64
79
  except StackError as error:
65
80
  assert error.status_code == 403
66
81
  else:
@@ -125,8 +140,15 @@ stack.agents.list()
125
140
  stack.agents.update(agent_id, **fields)
126
141
  stack.agents.unblock(agent_id)
127
142
 
143
+ # Missions (every passport is issued for exactly one Mission)
144
+ stack.missions.open(intent_summary, agent_id=None, intent_services=None, ...)
145
+ stack.missions.start(agent_id=agent_id, mission={"intent_summary": "..."}, passport=None)
146
+ stack.missions.complete(mission_id) # also revokes the Mission's passports
147
+ stack.missions.revoke(mission_id, reason="operator_initiated_clean")
148
+
128
149
  # Passports
129
- stack.passports.issue(agent_id, intent=None, services=None, ...)
150
+ stack.passports.issue(agent_id, mission_ids=[mission_id], intent=None, services=None, ...)
151
+ stack.passports.mission(agent_id, intent, services, ...) # context manager: Mission + passport, closed on exit
130
152
  stack.passports.verify(token)
131
153
  stack.passports.revoke(jti, reason=None)
132
154
  stack.passports.checkpoint(jti, services_used, actions_count, ...)
@@ -148,9 +170,9 @@ stack.reviews.list(status="flagged")
148
170
  stack.reviews.decide(checkout_id, decision, notes=None, block_future=False)
149
171
 
150
172
  # Drop-offs
151
- stack.dropoffs.create(from_agent_id, to_agent_id, schema, ...)
152
- stack.dropoffs.deposit(dropoff_id, data, agent_id)
153
- stack.dropoffs.collect(dropoff_id, agent_id)
173
+ stack.dropoffs.create(from_agent_id, to_agent_id, schema, ..., passport_token=None)
174
+ stack.dropoffs.deposit(dropoff_id, data, agent_id, passport_token=None)
175
+ stack.dropoffs.collect(dropoff_id, agent_id, passport_token=None)
154
176
 
155
177
  # Notifications
156
178
  stack.notifications.list()
@@ -161,6 +183,18 @@ stack.notifications.delete(channel_id)
161
183
  stack.audit.list(page=1, limit=50, action=None, agent_id=None)
162
184
  ```
163
185
 
186
+ ## Development
187
+
188
+ ```bash
189
+ python -m pip install -e ".[dev]"
190
+ ruff check .
191
+ ruff format --check .
192
+ mypy src
193
+ python -m pytest -q
194
+ ```
195
+
196
+ The package ships `py.typed`; `mypy src` runs in strict mode.
197
+
164
198
  ## Links
165
199
 
166
200
  - [Documentation](https://getstack.run/docs/sdk)
@@ -27,13 +27,24 @@ def run_fleet_mission_example(stack, request, provider_url="https://slack.com/ap
27
27
  repeated = stack.missions.start(request)
28
28
  assert repeated["mission_id"] == started["mission_id"]
29
29
  operation_id = hashlib.sha256(json.dumps([started["mission_id"], "support-update"]).encode()).hexdigest()
30
- response = stack.proxy.request("slack", provider_url, "POST",
30
+ response = stack.proxy.request(
31
+ "slack",
32
+ provider_url,
33
+ "POST",
31
34
  body={"channel": "C123", "text": "Support update"},
32
- passport_token=started["passport"]["token"], authority_request_id=operation_id)
35
+ passport_token=started["passport"]["token"],
36
+ authority_request_id=operation_id,
37
+ )
33
38
  assert response.status == 200
34
39
  try:
35
- stack.proxy.request("slack", provider_url, "POST", body={"channel": "OTHER", "text": "Support update"},
36
- passport_token=started["passport"]["token"], authority_request_id=operation_id + "_denied")
40
+ stack.proxy.request(
41
+ "slack",
42
+ provider_url,
43
+ "POST",
44
+ body={"channel": "OTHER", "text": "Support update"},
45
+ passport_token=started["passport"]["token"],
46
+ authority_request_id=operation_id + "_denied",
47
+ )
37
48
  except StackError as error:
38
49
  assert error.status_code == 403
39
50
  else:
@@ -98,8 +109,15 @@ stack.agents.list()
98
109
  stack.agents.update(agent_id, **fields)
99
110
  stack.agents.unblock(agent_id)
100
111
 
112
+ # Missions (every passport is issued for exactly one Mission)
113
+ stack.missions.open(intent_summary, agent_id=None, intent_services=None, ...)
114
+ stack.missions.start(agent_id=agent_id, mission={"intent_summary": "..."}, passport=None)
115
+ stack.missions.complete(mission_id) # also revokes the Mission's passports
116
+ stack.missions.revoke(mission_id, reason="operator_initiated_clean")
117
+
101
118
  # Passports
102
- stack.passports.issue(agent_id, intent=None, services=None, ...)
119
+ stack.passports.issue(agent_id, mission_ids=[mission_id], intent=None, services=None, ...)
120
+ stack.passports.mission(agent_id, intent, services, ...) # context manager: Mission + passport, closed on exit
103
121
  stack.passports.verify(token)
104
122
  stack.passports.revoke(jti, reason=None)
105
123
  stack.passports.checkpoint(jti, services_used, actions_count, ...)
@@ -121,9 +139,9 @@ stack.reviews.list(status="flagged")
121
139
  stack.reviews.decide(checkout_id, decision, notes=None, block_future=False)
122
140
 
123
141
  # Drop-offs
124
- stack.dropoffs.create(from_agent_id, to_agent_id, schema, ...)
125
- stack.dropoffs.deposit(dropoff_id, data, agent_id)
126
- stack.dropoffs.collect(dropoff_id, agent_id)
142
+ stack.dropoffs.create(from_agent_id, to_agent_id, schema, ..., passport_token=None)
143
+ stack.dropoffs.deposit(dropoff_id, data, agent_id, passport_token=None)
144
+ stack.dropoffs.collect(dropoff_id, agent_id, passport_token=None)
127
145
 
128
146
  # Notifications
129
147
  stack.notifications.list()
@@ -134,6 +152,18 @@ stack.notifications.delete(channel_id)
134
152
  stack.audit.list(page=1, limit=50, action=None, agent_id=None)
135
153
  ```
136
154
 
155
+ ## Development
156
+
157
+ ```bash
158
+ python -m pip install -e ".[dev]"
159
+ ruff check .
160
+ ruff format --check .
161
+ mypy src
162
+ python -m pytest -q
163
+ ```
164
+
165
+ The package ships `py.typed`; `mypy src` runs in strict mode.
166
+
137
167
  ## Links
138
168
 
139
169
  - [Documentation](https://getstack.run/docs/sdk)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getstack"
7
- version = "2.0.0"
7
+ version = "4.0.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -46,3 +46,36 @@ Repository = "https://github.com/getstack-run/sdk-python"
46
46
 
47
47
  [tool.hatch.build.targets.wheel]
48
48
  packages = ["src/getstack"]
49
+
50
+ [project.optional-dependencies]
51
+ dev = [
52
+ "pytest>=8.0.0",
53
+ "ruff>=0.9.0",
54
+ "mypy>=1.13.0",
55
+ ]
56
+
57
+ [tool.ruff]
58
+ line-length = 120
59
+ target-version = "py39"
60
+ src = ["src", "tests"]
61
+
62
+ [tool.ruff.lint]
63
+ select = [
64
+ "E", # pycodestyle errors
65
+ "W", # pycodestyle warnings
66
+ "F", # pyflakes
67
+ "I", # isort import order
68
+ "B", # flake8-bugbear
69
+ "UP", # pyupgrade
70
+ "SIM", # flake8-simplify
71
+ "C4", # flake8-comprehensions
72
+ "RUF", # ruff-specific rules
73
+ ]
74
+
75
+ [tool.ruff.lint.isort]
76
+ known-first-party = ["getstack"]
77
+
78
+ [tool.mypy]
79
+ strict = true
80
+ mypy_path = "src"
81
+ files = ["src"]