server4agent 0.1.0__tar.gz → 0.9.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 (35) hide show
  1. server4agent-0.9.0/CHANGELOG.md +170 -0
  2. server4agent-0.9.0/PKG-INFO +295 -0
  3. server4agent-0.9.0/README.md +269 -0
  4. server4agent-0.9.0/examples/await_input.py +30 -0
  5. server4agent-0.9.0/examples/exec_and_files.py +26 -0
  6. server4agent-0.9.0/examples/preview_env.py +51 -0
  7. server4agent-0.9.0/examples/webhook_receiver.py +35 -0
  8. {server4agent-0.1.0 → server4agent-0.9.0}/pyproject.toml +5 -1
  9. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/__init__.py +28 -0
  10. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/_async/client.py +301 -24
  11. server4agent-0.9.0/server4agent/_models.py +574 -0
  12. server4agent-0.9.0/server4agent/_sync/client.py +920 -0
  13. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/_transport.py +1 -1
  14. server4agent-0.9.0/server4agent/_version.py +1 -0
  15. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/errors.py +12 -4
  16. server4agent-0.9.0/tests/test_client.py +978 -0
  17. server4agent-0.1.0/CHANGELOG.md +0 -18
  18. server4agent-0.1.0/PKG-INFO +0 -151
  19. server4agent-0.1.0/README.md +0 -129
  20. server4agent-0.1.0/server4agent/_models.py +0 -197
  21. server4agent-0.1.0/server4agent/_sync/client.py +0 -567
  22. server4agent-0.1.0/server4agent/_version.py +0 -1
  23. server4agent-0.1.0/tests/test_client.py +0 -343
  24. {server4agent-0.1.0 → server4agent-0.9.0}/.gitignore +0 -0
  25. {server4agent-0.1.0 → server4agent-0.9.0}/LICENSE +0 -0
  26. {server4agent-0.1.0 → server4agent-0.9.0}/SECURITY.md +0 -0
  27. {server4agent-0.1.0 → server4agent-0.9.0}/examples/async_quickstart.py +0 -0
  28. {server4agent-0.1.0 → server4agent-0.9.0}/examples/quickstart.py +0 -0
  29. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/_async/__init__.py +0 -0
  30. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/_async/transport.py +0 -0
  31. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/_sync/__init__.py +0 -0
  32. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/_sync/transport.py +0 -0
  33. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/py.typed +0 -0
  34. {server4agent-0.1.0 → server4agent-0.9.0}/server4agent/webhooks.py +0 -0
  35. {server4agent-0.1.0 → server4agent-0.9.0}/tests/test_webhooks.py +0 -0
@@ -0,0 +1,170 @@
1
+ # Changelog
2
+
3
+ ## 0.9.0
4
+
5
+ Tuning runs are removed. The API no longer has the `/tuning/sessions`
6
+ endpoints, so neither client calls them. This is a breaking change.
7
+
8
+ - `client.tuning` is gone from both clients, with `list()`, `start()`, `get()`
9
+ and `report()`, and so are the `TuningSession` and `AsyncTuningSession`
10
+ handles and the `TuningStart` and `TuningStatus` types.
11
+ - `WEBHOOK_EVENTS` no longer lists `tuning.started` or `tuning.completed`, and
12
+ `webhooks.create()` with either is refused as an unknown event. Nothing
13
+ sends them any more. A webhook created before they were retired may still
14
+ have them stored; `webhooks.list()` leaves them off its `events`.
15
+
16
+ ## 0.8.0
17
+
18
+ Projects can put their URL behind a password.
19
+
20
+ - `ProjectAccess` gains `"password"`: a visitor to the project's URL is asked
21
+ for a password the owner set, and gets in without any account. A project
22
+ read from the API can now report it, so code that branches on `access`
23
+ should handle the new value.
24
+ - `projects.create()` and `project.update()` (sync and async, and the flat
25
+ `client.projects.update(project_id, ...)`) take `access_password`. It is
26
+ required when switching to `access="password"` unless a password is already
27
+ set, and refused with any other access. Sent on its own to a password
28
+ project, it changes the password and signs out everyone who used the old
29
+ one. Switching to another access discards it. It is at least 4 characters
30
+ (a 4 digit code works), stored encrypted, and never returned by the API.
31
+
32
+ ## 0.7.0
33
+
34
+ The client now covers every operation in the REST API. Everything below is
35
+ additive: code written against 0.6.0 keeps working unchanged.
36
+
37
+ New:
38
+
39
+ - Secrets, the environment variables injected into agent runs and bring-up.
40
+ `client.secrets.list()`, `set(key, value, description=None)` and
41
+ `delete(secret_id)` manage the account's secrets; `server.secrets` and
42
+ `project.secrets` (or `client.secrets.list_for_server()` /
43
+ `set_for_server()` / `list_for_project()` / `set_for_project()`) list and
44
+ bind them where they apply. Values are write-only: a `Secret` carries the
45
+ key, a note, a masked hint and its bindings, never the value.
46
+ - `server.logs(project_id=None, lines=None)`, `project.logs()` and
47
+ `client.servers.logs(server_id, ...)` return the log-file tails a server's
48
+ background processes write, or `None` before any were reported. When the
49
+ server cannot be read live the API answers with its last saved copy, so
50
+ check `LogsSnapshot.updated_at` before reading a tail as current.
51
+ - `project.start(command, name=None)` starts the project's app as a
52
+ background process on its routed port and keeps it running across restarts.
53
+ It returns an `AppStart` saying whether the app answered. When the app exits
54
+ instead, the `ConflictError` it raises carries the app's last output in
55
+ `err.body["log_tail"]`.
56
+ - `project.init(proceed=None)` starts bring-up for a project seeded from code,
57
+ or resumes one paused waiting for secrets.
58
+ - `project.export(ttl_seconds=None)` packs a project into one portable file
59
+ and returns a link to it, and `server.projects.import_bundle(bundle,
60
+ name=None)` recreates a project from that file, here or on another
61
+ deployment. `bundle` is the file's bytes or a base64 string. The result
62
+ lists the environment variables still to set in `missing_env`, because a
63
+ bundle carries their names and never their values.
64
+ - `client.tuning` starts, lists, reads and reports tuning runs, which fix an
65
+ agent from an evaluation platform's report. A `TuningSession` has `wait()`,
66
+ like a task or a build: reading a session is what advances it, so polling
67
+ is how it reaches `reported` or `failed`.
68
+ - `client.me()` returns the account the key acts on, its spend and budget this
69
+ period (`usage.blocked` says whether new tasks are refused), and how many
70
+ servers, projects and keys it has.
71
+ - `APIStatusError.body` is the whole JSON error response, for details beyond
72
+ the message, such as why a file write was refused (`reason`).
73
+
74
+ Catching up with the API:
75
+
76
+ - `server.deploy()` takes `project_id`, and `project.deploy()` deploys that
77
+ project. A server with several projects used to refuse every deploy from
78
+ this client with a 400, because there was no way to say which one. Deploy
79
+ now only puts a project online once something in it is listening, and
80
+ raises `ConflictError` when nothing is. It still returns the URL as a `str`,
81
+ which now also carries `.status` (`"live"`, or `"publishing"` when the URL
82
+ had not answered yet) and `.project_id`.
83
+ - `server.deployments()` raised `TypeError` on every call once the API began
84
+ returning each deployment's `project_id`. `Deployment.project_id` reads it.
85
+ - `webhooks.create(url)` without `events` sent `"events": null`, which the API
86
+ refuses. It now leaves the field out, which subscribes to every event.
87
+ `WEBHOOK_EVENTS` adds `quota.egress_cap_reached`, `tuning.started` and
88
+ `tuning.completed`.
89
+ - `server.exec()` takes `project_id`, to run inside that project's folder.
90
+ - `tasks.create()` takes `project_id`, and `follow_up_of` to continue an
91
+ earlier task's conversation. A task now reads `project_id`, `log` (the live
92
+ progress note), `change` (what the run committed) and `usage` (tokens, turns
93
+ and cost); one created as a follow-up also reads `follow_up_of` and
94
+ `resumed`.
95
+ - `servers.create()` and `projects.create()` take `secrets`, bound server-wide
96
+ or to the project. `projects.create()` also takes `source` (a template, a git
97
+ repository or an uploaded archive) and `auto_init`.
98
+ - Projects read `source`, `init_phase` and `run_plan`, plus `init_run_id` on
99
+ the handle that started a bring-up. `project.update()` takes `run_plan`.
100
+ - `files.get_download_url()` also returns the file's `path`.
101
+ - Templates read `source_kind`, and a build step's status can be `"failed"`.
102
+ - `ProjectAccess` and `ProjectVisibility` are exported from the package, as
103
+ 0.4.0 said they were.
104
+ - `examples/preview_env.py` builds the branch in the project's own folder,
105
+ starts the app with `project.start()` and puts it online with
106
+ `project.deploy()`. It used to build at the server root, so the project URL
107
+ it printed never had anything behind it.
108
+
109
+ ## 0.6.0
110
+
111
+ - `server.kind` on every server handle: `"standard"` for a headless box,
112
+ `"browser"` for one with a screen and a browser on it. The field was already
113
+ set at creation but no read path returned it, so a browser server was
114
+ indistinguishable from a headless one once you had it. A server from an API
115
+ that predates the field reads as `"standard"`.
116
+ - `kind` is accepted by `servers.create()` on both the sync and async clients,
117
+ which is what provisions a browser box. It needs a plan that allows the
118
+ medium tier, and costs more to run.
119
+
120
+ ## 0.5.0
121
+
122
+ - Requires Python 3.11 or newer. 3.9 is past end of life and 3.10 reaches it in
123
+ October 2026, so the floor now sits on the oldest release that still gets
124
+ upstream fixes. The client itself is unchanged: on 3.11 and newer, 0.5.0
125
+ behaves exactly as 0.4.0 did.
126
+
127
+ ## 0.4.0
128
+
129
+ - `project.access` and the `access` keyword on `projects.create()` and
130
+ `project.update()`. `"public"` is anyone with the link, which is what every
131
+ project did before and stays the default. `"team"` puts the project's URL
132
+ behind a Server4Agent sign-in, so only people in the account that owns it can
133
+ open it. A project from an API that predates the field reads as `"public"`.
134
+ - `ProjectAccess` is exported alongside `ProjectVisibility`. The two are
135
+ separate on purpose: `visibility` decides whether the URL serves at all,
136
+ `access` decides who gets through once it does.
137
+
138
+ ## 0.3.0
139
+
140
+ - `task.message_type`: `"user"` for a task you created, `"system"` for one the
141
+ platform started for you (a project imported from git brings itself up this
142
+ way). A system task's `goal` is a short summary of the run, not the
143
+ instructions its agent was given. Tasks from an API that predates the field
144
+ read as `"user"`.
145
+
146
+ ## 0.2.0
147
+
148
+ - `files.get_download_url(server_id, path, ttl_seconds=None)` — mint a
149
+ signed, expiring link anyone can use to download one workspace file, no
150
+ API key required to fetch it.
151
+ - `files.read_binary(server_id, path)` and `files.write(server_id, path,
152
+ content)` — read/write binary files (e.g. a generated `.xlsx`) without
153
+ corrupting bytes; `write` now also accepts `bytes` alongside `str`.
154
+
155
+ ## 0.1.0
156
+
157
+ Initial release.
158
+
159
+ - Sync `Server4Agent` and async `AsyncServer4Agent` clients.
160
+ - Resource-oriented handles: `create()`/`get()` return `Server`/`Project`/`Task`/`Build`
161
+ objects with scoped sub-resources (`server.tasks`, `server.files`, ...) and actions
162
+ (`server.exec()`, `server.deploy()`, `project.update()`).
163
+ - `task.wait()` / `build.wait()` poll until a terminal state, with an `on_poll` callback.
164
+ - Typed error hierarchy under `Server4AgentError` (`NotFoundError`, `RateLimitError`,
165
+ `APIStatusError`, ...) exposing `status`, `code`, and `request_id`.
166
+ - Automatic retries with exponential backoff + jitter; retries are method-aware so a
167
+ `POST` is never retried in a way that could double-create. Configurable `timeout`
168
+ and `max_retries`.
169
+ - Webhook signature verification (`verify_webhook_signature`).
170
+ - Ships `py.typed`. Opt-in request logging via `SERVER4AGENT_LOG=debug`.
@@ -0,0 +1,295 @@
1
+ Metadata-Version: 2.5
2
+ Name: server4agent
3
+ Version: 0.9.0
4
+ Summary: Python client for the Server4Agent REST API — sync + async, typed, with retries.
5
+ Project-URL: Homepage, https://www.server4agent.com
6
+ Project-URL: Documentation, https://www.server4agent.com/docs/sdks
7
+ Project-URL: Repository, https://github.com/Server4Agent/server4agent-python
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,mcp,sdk,server4agent
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx<1,>=0.24
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # server4agent
28
+
29
+ Python client for the [Server4Agent](https://www.server4agent.com) REST API —
30
+ sync **and** async, fully typed, with automatic retries.
31
+
32
+ This SDK is for the code *around* your agent — your backend, a script, a
33
+ notebook, a webhook receiver. If your agent itself does tool-calling, point it
34
+ at the [MCP server](https://www.server4agent.com/docs/mcp) directly; it doesn't
35
+ need this package.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install server4agent
41
+ ```
42
+
43
+ Requires Python 3.11+. Depends only on [`httpx`](https://www.python-httpx.org/).
44
+
45
+ ## Quickstart
46
+
47
+ ```python
48
+ from server4agent import Server4Agent
49
+
50
+ client = Server4Agent() # reads SERVER4AGENT_API_KEY from the environment
51
+
52
+ # create() returns a handle you can act on directly
53
+ server = client.servers.create(tier="small")
54
+
55
+ # kick off a build and block until it's live
56
+ build = server.builds.start("a FastAPI todo API with a web UI").wait()
57
+ print(build.url)
58
+
59
+ # run a command right on the server
60
+ print(server.exec("ls -la"))
61
+ ```
62
+
63
+ `api_key` can also be passed explicitly: `Server4Agent(api_key="sk_live_...")`.
64
+ Use it as a context manager to close the connection pool:
65
+
66
+ ```python
67
+ with Server4Agent() as client:
68
+ ...
69
+ ```
70
+
71
+ ## Async
72
+
73
+ The async client mirrors the sync one method-for-method:
74
+
75
+ ```python
76
+ import asyncio
77
+ from server4agent import AsyncServer4Agent
78
+
79
+ async def main():
80
+ async with AsyncServer4Agent() as client:
81
+ server = await client.servers.create(tier="small")
82
+ task = await server.tasks.create("Scrape today's HN front page to JSON.")
83
+ await task.wait()
84
+ print(task.status, task.result)
85
+
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ## Handles
90
+
91
+ `create()` and `get()` return rich handles — data records you can also act on:
92
+
93
+ ```python
94
+ server = client.servers.get("srv_abc")
95
+ server.tasks.create("...") # sub-resources are scoped to the server
96
+ server.files.write("app.py", "...")
97
+ server.deploy()
98
+ server.refresh() # re-fetch in place
99
+
100
+ project = client.projects.get("prj_xyz")
101
+ project.update(visibility="public")
102
+ # Ask visitors for a password before the URL opens. No account needed to get in.
103
+ project.update(access="password", access_password="a long shared passphrase")
104
+ ```
105
+
106
+ Binary files (e.g. a generated `.xlsx`) round-trip through `write`/`read_binary`
107
+ without corruption, and `get_download_url` mints a shareable, expiring link:
108
+
109
+ ```python
110
+ server.files.write("report.xlsx", xlsx_bytes) # bytes -> base64 on the wire
111
+ data = server.files.read_binary("report.xlsx") # decoded back to bytes
112
+
113
+ link = server.files.get_download_url("report.xlsx", ttl_seconds=3600)
114
+ print(link["url"], link["expires_at"])
115
+ ```
116
+
117
+ `server.kind` says what sort of box you are holding: `"standard"` is headless,
118
+ `"browser"` has a screen with a browser on it, for sites an agent has to drive
119
+ rather than fetch. It is orthogonal to `tier`, and a nickname proves nothing, so
120
+ read the field rather than the name:
121
+
122
+ ```python
123
+ server = client.servers.create(tier="medium", kind="browser")
124
+ with_screens = [s for s in client.servers.list() if s.kind == "browser"]
125
+ ```
126
+
127
+ `task.wait()` / `build.wait()` poll until a terminal state; both accept
128
+ `poll_interval` and `timeout` (seconds).
129
+
130
+ Not every task in a server's list is one you sent. `task.message_type` is
131
+ `"system"` for runs the platform starts for you, such as the bring-up of a
132
+ project imported from git, and `"user"` for your own. A system task's `goal` is
133
+ a summary of what the run does, not the instructions its agent was given:
134
+
135
+ ```python
136
+ mine = [t for t in server.tasks.list() if t.message_type == "user"]
137
+ ```
138
+
139
+ `project_id` runs a task in one project's folder, and `follow_up_of` continues
140
+ an earlier task's conversation, e.g. to answer one that is `awaiting_input`:
141
+
142
+ ```python
143
+ task = server.tasks.create("Add a dark mode toggle", project_id=project.id)
144
+ reply = server.tasks.create("Use the system setting by default", follow_up_of=task.id)
145
+ ```
146
+
147
+ ## Starting and deploying an app
148
+
149
+ `project.start()` runs the app as a background process with `PORT` set to the
150
+ project's routed port, keeps it running across restarts, and says whether it
151
+ answered. `deploy()` then makes the project public once something is listening:
152
+
153
+ ```python
154
+ from server4agent import ConflictError
155
+
156
+ try:
157
+ started = project.start("node server.js")
158
+ print(started.status, started.listening) # "running" True
159
+ except ConflictError as e:
160
+ print(e.body.get("log_tail")) # the app's last output
161
+
162
+ url = project.deploy() # or server.deploy(project_id=...)
163
+ print(url, url.status) # "live", or "publishing" if not answering yet
164
+ ```
165
+
166
+ `server.logs(project_id=None, lines=None)` (or `project.logs()`) returns the
167
+ log-file tails of the server's background processes, or `None` before any
168
+ were reported. When the server cannot be read live the API answers with its
169
+ last saved copy, so check `updated_at` before trusting a tail as current:
170
+
171
+ ```python
172
+ snap = project.logs(lines=50)
173
+ if snap:
174
+ print(snap.updated_at)
175
+ for f in snap.files:
176
+ print(f.path, f.tail)
177
+ ```
178
+
179
+ ## Secrets
180
+
181
+ Secrets are environment variables injected into agent runs and bring-up.
182
+ Values are write-only: reads return the key, a note, a masked hint and where it
183
+ is bound, never the value.
184
+
185
+ ```python
186
+ from server4agent import SecretInput
187
+
188
+ client.secrets.set("STRIPE_KEY", "sk_test_...", description="test mode")
189
+ server.secrets.set([{"key": "DATABASE_URL", "value": "postgres://..."}]) # every project on the server
190
+ project.secrets.set([SecretInput("SENTRY_DSN", "https://...")]) # this project only
191
+ print([s.key for s in project.secrets.list()])
192
+ ```
193
+
194
+ `servers.create()` and `projects.create()` also take `secrets=[...]`, so a
195
+ project seeded from git can come up without stopping to ask for them:
196
+
197
+ ```python
198
+ project = server.projects.create(
199
+ source={"kind": "git", "git_url": "https://github.com/northstar/app"},
200
+ secrets=[{"key": "DATABASE_URL", "value": "postgres://..."}],
201
+ )
202
+ print(project.init_phase, project.init_run_id) # bring-up starts on its own
203
+ project.init(proceed=True) # resume one waiting on secrets
204
+ ```
205
+
206
+ ## Moving a project
207
+
208
+ `export()` packs a project into one file behind a time-limited link, and
209
+ `import_bundle()` recreates it on any server, here or on another deployment.
210
+ A bundle carries the names of the environment variables it needs, never their
211
+ values:
212
+
213
+ ```python
214
+ import httpx
215
+
216
+ bundle = project.export(ttl_seconds=3600)
217
+ data = httpx.get(bundle.url).content
218
+
219
+ result = other_server.projects.import_bundle(data, name="landing-copy")
220
+ print(result.project.id, result.missing_env) # set these before the app can run
221
+ ```
222
+
223
+ ## Account and budget
224
+
225
+ ```python
226
+ me = client.me()
227
+ print(me.account["plan"], me.usage.spend_usd, me.usage.hard_limit_usd)
228
+ if me.usage.blocked:
229
+ print("new tasks are refused:", me.usage.reason)
230
+ ```
231
+
232
+ ## Errors
233
+
234
+ Every failure is a subclass of `Server4AgentError`, so you can catch broadly or
235
+ narrowly:
236
+
237
+ ```python
238
+ from server4agent import NotFoundError, RateLimitError, APIStatusError
239
+
240
+ try:
241
+ client.servers.get("srv_missing")
242
+ except NotFoundError:
243
+ ... # 404
244
+ except RateLimitError as e:
245
+ ... # 429 (already retried; quote e.request_id to support)
246
+ except APIStatusError as e:
247
+ print(e.status, e.code, e.message, e.request_id)
248
+ ```
249
+
250
+ Retryable failures (429, 5xx, network blips) are retried automatically with
251
+ exponential backoff. Tune it per client:
252
+
253
+ ```python
254
+ Server4Agent(timeout=30.0, max_retries=4)
255
+ ```
256
+
257
+ ## Verifying webhooks
258
+
259
+ ```python
260
+ import os
261
+ from server4agent import verify_webhook_signature
262
+
263
+ # request.data must be the raw body — parse it as JSON only after verifying.
264
+ ok = verify_webhook_signature(
265
+ secret=os.environ["SERVER4AGENT_WEBHOOK_SECRET"],
266
+ body=request.data.decode("utf-8"),
267
+ header=request.headers.get("Server4Agent-Signature"),
268
+ )
269
+ if not ok:
270
+ return "invalid signature", 401
271
+ ```
272
+
273
+ ## Examples
274
+
275
+ Runnable scripts in [`examples/`](examples/):
276
+
277
+ | File | What it shows |
278
+ | --- | --- |
279
+ | [`quickstart.py`](examples/quickstart.py) | Provision a server, build an app, print the live URL. |
280
+ | [`async_quickstart.py`](examples/async_quickstart.py) | The same flow on the async client. |
281
+ | [`exec_and_files.py`](examples/exec_and_files.py) | Write a file, install a dependency, and run it on the durable workspace. |
282
+ | [`await_input.py`](examples/await_input.py) | Handle a task that pauses to ask a question (`awaiting_input`). |
283
+ | [`preview_env.py`](examples/preview_env.py) | Ephemeral per-PR preview URL for CI. |
284
+ | [`webhook_receiver.py`](examples/webhook_receiver.py) | Verify webhook signatures in a Flask handler. |
285
+
286
+ ## API surface
287
+
288
+ `servers`, `projects`, `templates`, `tasks`, `builds`, `files`, `secrets`,
289
+ `keys`, `webhooks`, and `client.me()`. Every REST operation has a
290
+ method on both clients; see the
291
+ [REST API docs](https://www.server4agent.com/docs/api) for the endpoints each
292
+ one wraps.
293
+
294
+ Server-side only: this SDK holds your `sk_live_` key. Never embed it in a
295
+ notebook or script you share.