boxd 0.2.18.dev896__tar.gz → 0.2.19__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.
- {boxd-0.2.18.dev896/src/boxd.egg-info → boxd-0.2.19}/PKG-INFO +322 -14
- {boxd-0.2.18.dev896 → boxd-0.2.19}/README.md +320 -13
- {boxd-0.2.18.dev896 → boxd-0.2.19}/pyproject.toml +5 -1
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/__init__.py +51 -0
- boxd-0.2.19/src/boxd/_generated/api_pb2.py +626 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_generated/api_pb2_grpc.py +47 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_mappers.py +73 -1
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_requests.py +24 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/errors.py +5 -1
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/models.py +169 -3
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/machines.py +288 -5
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/vars.py +6 -3
- boxd-0.2.19/src/boxd/spec.py +502 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19/src/boxd.egg-info}/PKG-INFO +322 -14
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd.egg-info/SOURCES.txt +3 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd.egg-info/requires.txt +1 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_mappers.py +13 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_namespaces.py +25 -0
- boxd-0.2.19/tests/test_python_versions.py +115 -0
- boxd-0.2.19/tests/test_spec.py +639 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_transport.py +4 -0
- boxd-0.2.18.dev896/src/boxd/_generated/api_pb2.py +0 -600
- {boxd-0.2.18.dev896 → boxd-0.2.19}/LICENSE +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/setup.cfg +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_client.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_credentials.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_transport.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_urls.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/_version_check.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/domains.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/orgs.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/snapshots.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd/resources/teams.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_credentials.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_delete_self.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_requests.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_streaming.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_urls.py +0 -0
- {boxd-0.2.18.dev896 → boxd-0.2.19}/tests/test_version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.19
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ License-File: LICENSE
|
|
|
23
23
|
Requires-Dist: grpcio>=1.80
|
|
24
24
|
Requires-Dist: protobuf>=6.31.1
|
|
25
25
|
Requires-Dist: pydantic>=2
|
|
26
|
+
Requires-Dist: typing-extensions>=4.12
|
|
26
27
|
Requires-Dist: httpx<1,>=0.27
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: grpcio-tools<=1.80,>=1.60; extra == "dev"
|
|
@@ -250,6 +251,284 @@ private to you unless you pass `shared=True`.
|
|
|
250
251
|
usable. Call `wait_until_ready` before doing anything that depends on it
|
|
251
252
|
running — especially before forking it again.
|
|
252
253
|
|
|
254
|
+
### Machine specs
|
|
255
|
+
|
|
256
|
+
A machine spec describes a machine in one value: what it boots from, its size,
|
|
257
|
+
labels, networks, isolation, egress, env, the secrets it gets, the
|
|
258
|
+
integrations and agent logins it needs, and the packages it installs on first boot. Build one from reusable pieces and create from it:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from boxd import Machine, team, snapshot, github, slack, jira, claude, secrets
|
|
262
|
+
|
|
263
|
+
engineering = (
|
|
264
|
+
Machine.spec()
|
|
265
|
+
.from_(team("eng")) # a team's default snapshot; or snapshot("eng-base")
|
|
266
|
+
.network("eng")
|
|
267
|
+
.label("team", "eng")
|
|
268
|
+
.env("NODE_ENV", "development")
|
|
269
|
+
.packages("apt-get update", "apt-get install -y nginx")
|
|
270
|
+
.with_(github(shared="eng-bot"), claude())
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
mike = engineering.size("2x8").with_(slack(), jira(shared="jira"), secrets("NPM_TOKEN"))
|
|
274
|
+
m = boxd.machines.create("mike-dev", org="acme", spec=mike)
|
|
275
|
+
m.spec # the spec as it stands (update_spec, and label, network, egress and size edits, update it)
|
|
276
|
+
m.created_spec # the spec it was created with, never changed
|
|
277
|
+
mike.resolve() # the same spec as a dict, without creating anything
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
| Builder | Sets |
|
|
281
|
+
|---|---|
|
|
282
|
+
| `.from_(team(name))` / `.from_(snapshot(name))` | `base`. Omit for the default image. |
|
|
283
|
+
| `.size("1x4" \| "2x8" \| "4x16" \| …)` | `size`, `<vcpu>x<memory GiB>` |
|
|
284
|
+
| `.assign_to(user_id)` | `assignTo`: create the machine for another member of the org (below) |
|
|
285
|
+
| `.label(key, value)` / `.label({...})` | `labels` |
|
|
286
|
+
| `.network(*names)` | `networks` |
|
|
287
|
+
| `.isolated()` | `isolated` |
|
|
288
|
+
| `.egress(*hosts)` | `egress`: the machine's egress allowlist from birth |
|
|
289
|
+
| `.env(key, value)` / `.env({...})` | `env`: wins over org and personal vars of the same name |
|
|
290
|
+
| `.packages(*commands)` | `packages`: shell commands run once after the first boot (below) |
|
|
291
|
+
| `.with_(*pieces)` | merges pieces or other builders |
|
|
292
|
+
|
|
293
|
+
Pieces: `slack()`, `jira()`, `github()`, `asana()` (pass `shared="tag"` to use
|
|
294
|
+
the org connection shared under that tag instead of the owner's own),
|
|
295
|
+
`claude()`, `codex()`, and `secrets(*names)` (below). Integrations and agent logins are recorded on the
|
|
296
|
+
machine; a missing one never blocks the create (see "Integrations the machine
|
|
297
|
+
needs"). A piece is a plain dict (`{"labels": {...}}`, or
|
|
298
|
+
`{"connections": {"notion": "personal"}}` for any toolkit), so you can write your own.
|
|
299
|
+
|
|
300
|
+
Pieces merge in order: maps (`labels`, `env`, `connections`) merge with the
|
|
301
|
+
later piece winning, lists (`networks`, `egress`, `secrets`, `agents`,
|
|
302
|
+
`packages`) combine in order without duplicates, `isolated` only turns on, and `base`, `size` or `assignTo`
|
|
303
|
+
set twice to different values raises `MachineSpecError` (a `ValueError`). So
|
|
304
|
+
does a key a spec doesn't have, and a name in both `env` and `secrets`. The server checks every field and names it in its error
|
|
305
|
+
(`spec.size: ...`).
|
|
306
|
+
|
|
307
|
+
A spec carries its machine's base, size, labels, networks, isolation and env,
|
|
308
|
+
so `create(spec=...)` takes none of `image`, `env`, `labels`, `networks`,
|
|
309
|
+
`isolated`, `vcpu` or `memory` beside it. A spec whose base is a snapshot (or a
|
|
310
|
+
team with a default snapshot) restores it. A snapshot keeps its machine's spec:
|
|
311
|
+
restoring it gives the new machine that spec, and
|
|
312
|
+
`create(from_snapshot=..., spec=...)` merges a spec on top by the same rules;
|
|
313
|
+
its `size`, if set, must be the snapshot's. A fork keeps its source's spec.
|
|
314
|
+
|
|
315
|
+
#### Secrets
|
|
316
|
+
|
|
317
|
+
A spec names the secrets its machine gets; their values stay in
|
|
318
|
+
`boxd.secrets`. Usually they are `unattached` secrets, which reach no machine
|
|
319
|
+
until a spec lists them:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
boxd.secrets.set("NPM_TOKEN", "npm_…", scope="unattached", org="acme")
|
|
323
|
+
|
|
324
|
+
m = boxd.machines.create("mike-dev", org="acme", spec=engineering.with_(secrets("NPM_TOKEN")))
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
A name resolves, on your own machine, to your own secret first
|
|
328
|
+
(`private-unattached`, then `private`), else the org's (`unattached`, `all`,
|
|
329
|
+
`shared`). On a machine created for someone else, or a shared machine, only
|
|
330
|
+
the org's secrets count. Only org owners and admins may list an org
|
|
331
|
+
`unattached` secret in a spec, and an org `shared` secret only reaches
|
|
332
|
+
shared machines, so a private machine can't list one. A name that resolves to
|
|
333
|
+
nothing, or that the create may not list, fails the create, naming it.
|
|
334
|
+
|
|
335
|
+
Only org owners and admins may set, move or delete an org `unattached` secret;
|
|
336
|
+
your own (`private-unattached`) are yours. From inside a machine (with its
|
|
337
|
+
in-VM credentials) a spec can't list an unattached secret, yours or the org's,
|
|
338
|
+
on any machine, and `update_spec` can't change that machine's own `egress` or
|
|
339
|
+
`secrets`; make those changes from outside the machine, with an API key or in
|
|
340
|
+
the console. Names a fork or a restore takes over from its source's spec are
|
|
341
|
+
not affected.
|
|
342
|
+
|
|
343
|
+
On the machine, the spec's `env` and its `secrets` win over your `private`
|
|
344
|
+
vars, which win over the org's `shared` vars (shared machines only), which win
|
|
345
|
+
over the org's `all` vars. A name can't be in both `env` and `secrets`.
|
|
346
|
+
|
|
347
|
+
The machine keeps a reference, not a copy: rotating the secret reaches it, and
|
|
348
|
+
deleting the secret takes it away
|
|
349
|
+
(setting it again with the same name brings it back). Forks, snapshots and
|
|
350
|
+
restores resolve the
|
|
351
|
+
names again, and fail naming any that no longer resolve. A secret's
|
|
352
|
+
`bound_machines` says how many machines list it.
|
|
353
|
+
|
|
354
|
+
#### Creating a machine for someone else
|
|
355
|
+
|
|
356
|
+
An org owner or admin can create a machine for another member of the org with
|
|
357
|
+
`.assign_to(user_id)` (the member's user id, as `orgs.get_members()` lists it):
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
m = boxd.machines.create(
|
|
361
|
+
"mike-dev",
|
|
362
|
+
org="acme", # required: the org the machine is for
|
|
363
|
+
spec=engineering.size("2x8").assign_to("usr_e393f70e…"),
|
|
364
|
+
)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
The machine is the member's from the start: private to them, billed to the
|
|
368
|
+
org, and set up with their own integrations and agent logins. You can't see or
|
|
369
|
+
reach it afterwards, so `create()` answers with its id and name only, and the
|
|
370
|
+
create can't also set `delete_after`. The member sees `created_by` (your user
|
|
371
|
+
id) and `created_by_name` on it. Refused, naming `spec.assignTo`: without
|
|
372
|
+
`org`, with `shared=True`, for a caller who isn't an owner or admin of the org,
|
|
373
|
+
with an org API key or from inside a machine, for someone who isn't a member,
|
|
374
|
+
and with a snapshot or disk the member couldn't use. Your own id is a plain
|
|
375
|
+
create. `assignTo` is never inherited: forks, snapshots and restores drop it.
|
|
376
|
+
|
|
377
|
+
The machine runs the spec you wrote as the member's own machine: its packages
|
|
378
|
+
as root, its env and its base, alongside the member's GitHub token, agent
|
|
379
|
+
logins and personal connections. Assign a spec only if the member would run it
|
|
380
|
+
themselves. The member sees who created it (`created_by`) and can read the
|
|
381
|
+
spec (`machine.spec`).
|
|
382
|
+
|
|
383
|
+
#### Integrations the machine needs
|
|
384
|
+
|
|
385
|
+
A spec's `connections` and `agents` are the machine's requirements. Reading the
|
|
386
|
+
machine (`get`, `list`, the machine `create` returns) tells you, for each one,
|
|
387
|
+
whether it is connected right now:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
m = boxd.machines.get("mike-dev")
|
|
391
|
+
for r in m.requirements:
|
|
392
|
+
# kind="connection" name="jira" scope="shared" tag="jira" met=False can_fix=True reason=None
|
|
393
|
+
print(r.name, r.scope, r.tag, "connected" if r.met else "connect it" if r.can_fix else r.reason or "ask an org admin")
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
A `personal` connection is the machine owner's own connection for that toolkit
|
|
397
|
+
in the machine's org (on a machine created with `assign_to`, the member's), a
|
|
398
|
+
`shared` one the org's connection shared under `tag`, and an agent the owner's
|
|
399
|
+
Claude Code or Codex sign-in. `can_fix` says whether you could connect it: the
|
|
400
|
+
owner for personal ones and agents, an org owner or admin for shared ones.
|
|
401
|
+
Connect from the console: the machine's page lists them with a Connect button.
|
|
402
|
+
|
|
403
|
+
An unmet requirement is only unavailable on the machine; nothing blocks
|
|
404
|
+
creating, booting or using it. Some can't be met on the machine as it is, and
|
|
405
|
+
`reason` says why: an isolated machine gets no integrations or agent logins,
|
|
406
|
+
and a shared machine never gets anyone's personal connection or agent login
|
|
407
|
+
(require `shared:<tag>` instead). The create refuses a toolkit this cluster
|
|
408
|
+
can't connect (`spec.connections.<toolkit>: ...`), and the GitHub App
|
|
409
|
+
(`github-app`) only as `"shared:org"`.
|
|
410
|
+
|
|
411
|
+
#### Packages
|
|
412
|
+
|
|
413
|
+
`packages` are shell commands the machine runs once, in order, after its first
|
|
414
|
+
boot: as root, with bash (sh on an image without it),
|
|
415
|
+
`DEBIAN_FRONTEND=noninteractive`, no stdin, the env and secrets an exec
|
|
416
|
+
session on the machine gets, and a 30-minute timeout each. Their output is
|
|
417
|
+
appended to `/var/log/boxd/packages.log` in the machine, which only root can
|
|
418
|
+
read. A command is done when its shell exits, so one that leaves a process
|
|
419
|
+
running in the background (`my-server &`) doesn't hold up the next. The first
|
|
420
|
+
failure (a non-zero exit, a timeout) stops the rest; the machine stays usable,
|
|
421
|
+
and `update_spec(id, {}, retry_packages=True)` runs the failed command and the ones after it again (so does
|
|
422
|
+
Retry in the console's Packages section). To give up on a command instead, or
|
|
423
|
+
to replace a wrong one, remove it from the spec: a command that leaves the spec
|
|
424
|
+
before it finished is skipped, and the ones after it run. Reading the machine
|
|
425
|
+
tells you how each one went:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
m = boxd.machines.get("mike-dev")
|
|
429
|
+
for p in m.packages:
|
|
430
|
+
# command="apt-get install -y nginx" state="failed" exit_code=100 output_tail="E: Unable to locate…" reason=None …
|
|
431
|
+
print(p.state, p.exit_code, p.command)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
`state` is `"pending"`, `"running"`, `"done"`, `"failed"` or `"skipped"`; the
|
|
435
|
+
commands after a failure stay `"pending"`. A pending or failed command that an
|
|
436
|
+
`update_spec` removes from the spec becomes `"skipped"`: it never runs and no
|
|
437
|
+
longer stops the ones after it, which run right away (or when the machine next
|
|
438
|
+
runs). A skipped one that failed keeps its `exit_code` and `output_tail`, and
|
|
439
|
+
`reason` says when it was skipped. Listing it in the spec again puts it back to
|
|
440
|
+
`"pending"`, in its place. So replacing a broken command is one patch: drop it
|
|
441
|
+
and add the corrected one, and the corrected one runs, with everything that
|
|
442
|
+
was waiting. A running command can't be removed (the patch is refused until it
|
|
443
|
+
ends), and removing a done one undoes nothing. `output_tail` is the last 4 KiB of the
|
|
444
|
+
command's output, with the value of every secret the machine gets shown as
|
|
445
|
+
`[secret NAME]` (only the value as it is: printed encoded or in pieces, it isn't
|
|
446
|
+
recognised). The log file isn't masked: it is root's, and root on the machine
|
|
447
|
+
can read its env anyway. `output_tail` is empty on the machines `list()` returns,
|
|
448
|
+
which leave it out to keep the list small; `get()` has it. `reason` says why a failed one has no exit code (it
|
|
449
|
+
timed out, or was interrupted too often), or why a pending one is waiting. A
|
|
450
|
+
command the platform interrupts (the machine stops, reboots, hibernates or
|
|
451
|
+
moves, or boxd restarts on the machine's host) goes back to `"pending"` and runs
|
|
452
|
+
again from the start once the machine is running, and the rest follow; after 3
|
|
453
|
+
interruptions it fails. What finished never runs again: not on a reboot, a
|
|
454
|
+
resume, a wake or a move. Restoring a snapshot marks done the commands that had
|
|
455
|
+
run on the machine the snapshot was taken from and runs the snapshot spec's
|
|
456
|
+
others (one that was running when the snapshot was taken runs again from the
|
|
457
|
+
start), then the ones the restore's spec adds; a fork keeps its source's states
|
|
458
|
+
and runs nothing. The machine doesn't auto-suspend or
|
|
459
|
+
hibernate while a command runs. At most 50 commands (a restore counts the
|
|
460
|
+
snapshot's too), each at most 4 KiB; the server names the one it refuses
|
|
461
|
+
(`spec.packages[2]: ...`). Commands that download something need the hosts in
|
|
462
|
+
the spec's `egress`, if it has one.
|
|
463
|
+
|
|
464
|
+
A spec has size limits, checked before anything else and named with what was
|
|
465
|
+
given (`spec.env: at most 128 variables (got 129)`): at most 256 KiB as JSON
|
|
466
|
+
in all; `env` at most 128 variables, each value at most 32 KiB and all values
|
|
467
|
+
together at most 64 KiB; `secrets` at most 64 names; `networks` at most 16, each
|
|
468
|
+
name at most 63 bytes; `egress` at most 256 entries, none empty (leave `egress`
|
|
469
|
+
out for no allowlist); `connections` at most 64; `packages` at most 200 listed
|
|
470
|
+
and 50 once duplicates are dropped. An `update_spec` patch is at most 256 KiB.
|
|
471
|
+
|
|
472
|
+
#### Updating a machine's spec
|
|
473
|
+
|
|
474
|
+
`update_spec` changes the spec of a machine created from one and applies what
|
|
475
|
+
changes. Pass a JSON Merge Patch, or a function that adds to the current spec:
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
# Patch: a field left out is untouched, a scalar replaces, maps merge key by
|
|
479
|
+
# key (None removes a key), a list replaces the whole list, None removes a field.
|
|
480
|
+
boxd.machines.update_spec("mike-dev", {
|
|
481
|
+
"labels": {"owner": "mike", "team": None},
|
|
482
|
+
"networks": ["eng", "ops"],
|
|
483
|
+
"packages": ["apt-get install -y nginx", "npm i -g pnpm"],
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
# Builder: reads the spec, adds with the composition rules, sends the difference.
|
|
487
|
+
boxd.machines.update_spec("mike-dev", lambda s: s.network("ops").env("LOG_LEVEL", "debug"))
|
|
488
|
+
|
|
489
|
+
# Dry run: the diff, nothing changed.
|
|
490
|
+
diff = boxd.machines.update_spec("mike-dev", {"size": "4x16"}, dry_run=True)
|
|
491
|
+
for c in diff.changes:
|
|
492
|
+
print(c.field, c.apply, c.note, c.warnings)
|
|
493
|
+
if diff.reboot_required:
|
|
494
|
+
boxd.machines.update_spec("mike-dev", {"size": "4x16"}, reboot=True)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Each change in `changes` says how it applies (`apply`):
|
|
498
|
+
|
|
499
|
+
| Field | `apply` |
|
|
500
|
+
|---|---|
|
|
501
|
+
| `labels`, `networks`, `egress` | `"live"` (new egress rules are in force within about a second of the call returning) |
|
|
502
|
+
| `connections`, `agents` | `"live"`: what the machine needs (`requirements`) changes |
|
|
503
|
+
| `packages` | `"live"`: added commands run now, after the ones before them. Removing one undoes nothing (a warning says so); a pending or failed one the patch removes is skipped (listed in `skipped`), so the ones after it run, and a skipped one listed again goes back to pending in its place (`unskipped`); a command that already ran isn't run again; commands after a failed one don't run (a warning says so) unless the update also retries (`retry_packages=True`: the failed command and the rest run again first, listed in `changed`, skipped ones excepted; a failed command the patch removes is skipped, not retried). Removing a running command is `"refused"` until it ends |
|
|
504
|
+
| `env`, `secrets` | `"reboot_recommended"`: new shells and exec sessions get them now; processes already running keep the old values until they restart (`reboot_recommended`) |
|
|
505
|
+
| `size` | `"reboot"` on a running machine, which reboots to apply it (as `resize`), so it needs `reboot=True`; `"live"` on a stopped one |
|
|
506
|
+
| `base`, `isolated`, `assignTo` | `"refused"`: fixed when the machine is created |
|
|
507
|
+
|
|
508
|
+
A refused change (also: new packages or a retry on a fork, a size change in standby or
|
|
509
|
+
hibernated) refuses the whole patch, and so does a reboot without
|
|
510
|
+
`reboot=True`; nothing is applied. So is an update during which the spec
|
|
511
|
+
changed (another update, a label edit): it raises `ConflictError`, and
|
|
512
|
+
reading the machine again and retrying is safe. The builder form does that
|
|
513
|
+
itself: it sends the spec it read, so a change made in between can't be
|
|
514
|
+
undone by its lists, and on a conflict reads the spec again and reapplies the
|
|
515
|
+
function, up to 3 times. A patch you built from a spec you read can say so
|
|
516
|
+
too, `expected_spec=machine.spec`: if the spec is no longer that one, nothing
|
|
517
|
+
is applied and it raises `ConflictError`. Otherwise the spec and the labels,
|
|
518
|
+
networks, egress and secrets it drives change together, then the size, then
|
|
519
|
+
the new packages run. If the size then fails (the org's quota changed, the
|
|
520
|
+
machine went to standby), the rest stays applied and the call still returns:
|
|
521
|
+
`resize_error` says why, and `spec` has the size the machine has. The patched spec is checked like a create's (an
|
|
522
|
+
unknown toolkit, a secret you may not list, more than 50 commands in all are
|
|
523
|
+
refused, naming the field). `machine.created_spec` never changes, so the
|
|
524
|
+
console shows the spec badge as "updated". Allowed to whoever may edit the
|
|
525
|
+
machine's labels: its owner, or an org owner or admin on a shared machine.
|
|
526
|
+
The builder form only adds: a `size` that differs from the spec's raises
|
|
527
|
+
(`"2X8"` is the same as `"2x8"`), so
|
|
528
|
+
use a patch to replace or remove. `spec_patch(from_spec, to_spec)` computes the
|
|
529
|
+
patch the builder form sends. `AsyncBoxd` has the same `await
|
|
530
|
+
boxd.machines.update_spec(...)`.
|
|
531
|
+
|
|
253
532
|
### Egress allowlist
|
|
254
533
|
|
|
255
534
|
A machine can be limited to what it may reach on the internet:
|
|
@@ -260,13 +539,16 @@ boxd.machines.get("alpha").egress_allow
|
|
|
260
539
|
boxd.machines.set_egress_allow("alpha", []) # clear: unrestricted again
|
|
261
540
|
```
|
|
262
541
|
|
|
263
|
-
Entries are hostnames or wildcards, or public IPv4 addresses and CIDRs
|
|
542
|
+
Entries are hostnames or wildcards, or public IPv4 addresses and CIDRs, at
|
|
543
|
+
most 256; an internationalized name is kept in its ASCII form (`bücher.de`
|
|
544
|
+
becomes `xn--bcher-kva.de`), which is what the machine looks up. Web
|
|
264
545
|
requests are admitted by hostname, everything else by address, with the
|
|
265
546
|
addresses of allowlisted names learned as the machine resolves them. Nothing
|
|
266
547
|
that is not named is reachable. The hosts of any bound secret the machine
|
|
267
|
-
holds are always admitted. Takes effect within a second
|
|
268
|
-
|
|
269
|
-
machine restored from a snapshot starts unrestricted
|
|
548
|
+
holds are always admitted. Takes effect within a second. From inside the
|
|
549
|
+
machine it can be read (`boxd machine egress`) but not changed. A fork inherits its source's allowlist; a
|
|
550
|
+
machine restored from a snapshot starts unrestricted, unless its machine spec
|
|
551
|
+
lists `egress`.
|
|
270
552
|
|
|
271
553
|
### Labels
|
|
272
554
|
|
|
@@ -345,6 +627,15 @@ machine.source.id # str | None — provenance; may not resolve
|
|
|
345
627
|
machine.hibernated_at # datetime | None — None = not hibernated
|
|
346
628
|
machine.last_connected_at # datetime | None — None = never connected
|
|
347
629
|
machine.boot_time_ms # int | None — last boot; None = never booted
|
|
630
|
+
machine.spec # dict | None — the machine spec it runs under
|
|
631
|
+
machine.created_spec # dict | None — the spec it was created with
|
|
632
|
+
machine.created_by # str | None — who created it for you (spec assignTo); None = you did
|
|
633
|
+
machine.created_by_name # str | None — a name to show for created_by
|
|
634
|
+
machine.requirements # list[MachineRequirement] — the spec's connections and agents:
|
|
635
|
+
# kind, name, scope ("personal"/"shared"), tag, met, can_fix, reason
|
|
636
|
+
machine.packages # list[MachinePackage] — the spec's packages and how each went:
|
|
637
|
+
# command, state, exit_code, output_tail, reason, started_at, finished_at
|
|
638
|
+
# (output_tail is empty from list(): get() has it)
|
|
348
639
|
```
|
|
349
640
|
|
|
350
641
|
`None` always means "not set": a port that was never allocated, a boot that
|
|
@@ -558,23 +849,33 @@ boxd.env.list(org="acme")
|
|
|
558
849
|
boxd.env.delete("MODE", scope="all")
|
|
559
850
|
|
|
560
851
|
boxd.secrets.set("API_TOKEN", "s3cr3t", scope="shared")
|
|
561
|
-
boxd.secrets.list() # Secret(name, scope, domains) — no value
|
|
852
|
+
boxd.secrets.list() # Secret(name, scope, domains, bound_machines) — no value
|
|
562
853
|
boxd.secrets.delete("API_TOKEN", scope="shared")
|
|
563
854
|
```
|
|
564
855
|
|
|
565
856
|
A secret can be bound to the hosts it may be sent to. The machine then only
|
|
566
|
-
ever holds a placeholder, and the real value is substituted into
|
|
567
|
-
those hosts on the way out:
|
|
857
|
+
ever holds a placeholder, and the real value is substituted into HTTPS
|
|
858
|
+
requests to those hosts on the way out:
|
|
568
859
|
|
|
569
860
|
```python
|
|
570
861
|
boxd.secrets.set("STRIPE_KEY", "sk_live_...", scope="all", domains=["api.stripe.com"])
|
|
571
862
|
```
|
|
572
863
|
|
|
573
|
-
Inside the machine, `STRIPE_KEY` is an opaque `bxds_…` string.
|
|
574
|
-
`api.stripe.com` carrying it
|
|
575
|
-
arrives at Stripe with the real key
|
|
576
|
-
|
|
577
|
-
|
|
864
|
+
Inside the machine, `STRIPE_KEY` is an opaque `bxds_…` string. An HTTPS
|
|
865
|
+
request to `api.stripe.com` carrying it anywhere (the URL, a header, Basic auth
|
|
866
|
+
included, or the body) arrives at Stripe with the real key. It is replaced only
|
|
867
|
+
over HTTPS, and only when both the server name the connection was opened for
|
|
868
|
+
and the request's own `Host` are hosts the secret is bound to: over plain HTTP
|
|
869
|
+
and in a request to any other host it stays the useless placeholder. A body
|
|
870
|
+
over 16 MiB with a `Content-Length` is sent as written, placeholder included.
|
|
871
|
+
Responses on those connections carry the placeholder instead of the value:
|
|
872
|
+
wherever the real value (8 characters or more) appears in a response's headers
|
|
873
|
+
or body exactly as it was sent, it is replaced, so a server that echoes the
|
|
874
|
+
request doesn't hand the value to the machine. A value the server sends back
|
|
875
|
+
re-encoded (base64, URL- or JSON-escaped) isn't caught, and neither is one in
|
|
876
|
+
a body compressed with anything but gzip or deflate (the proxy asks for an
|
|
877
|
+
uncompressed response). Nothing to configure in the machine, and the
|
|
878
|
+
placeholder does not change when the value is rotated. `domains` is the full desired set:
|
|
578
879
|
setting the secret again without it makes it a plain secret. Wildcards such as
|
|
579
880
|
`*.stripe.com` are accepted; provider-wide ones such as `*.amazonaws.com` are
|
|
580
881
|
refused.
|
|
@@ -595,6 +896,13 @@ Scope decides which machines a name applies to:
|
|
|
595
896
|
| `private` | only your machines in that organization |
|
|
596
897
|
| `shared` | the organization's shared machines |
|
|
597
898
|
| `all` | every machine in the organization |
|
|
899
|
+
| `unattached` | secrets only: just the machines whose spec lists it ([Secrets](#secrets)); the organization's with `org` (set, moved and deleted by its owners and admins only), else your own |
|
|
900
|
+
| `private-unattached` | secrets only: your own unattached secret in the organization named by `org` |
|
|
901
|
+
|
|
902
|
+
`list` reports the organization's unattached secrets as `unattached` and your
|
|
903
|
+
own as `private-unattached`, each with `bound_machines`, the number of machines
|
|
904
|
+
whose spec lists it (0 for every other scope). `unattached` is for secrets
|
|
905
|
+
only: a plain value a machine needs belongs in its spec's `env`.
|
|
598
906
|
|
|
599
907
|
Scope is part of a name's identity — the same name can exist in several scopes
|
|
600
908
|
at once — so changing it is a `move` between two addresses, and both ends are
|
|
@@ -860,7 +1168,7 @@ from boxd import (
|
|
|
860
1168
|
AuthenticationError, # no usable credential, or it was rejected
|
|
861
1169
|
PermissionDeniedError, # authenticated, but not allowed
|
|
862
1170
|
NotFoundError,
|
|
863
|
-
ConflictError, # already exists,
|
|
1171
|
+
ConflictError, # already exists, fights the current state, or it changed meanwhile
|
|
864
1172
|
RateLimitError, # rate limit or quota
|
|
865
1173
|
APIStatusError, # any other error from the server
|
|
866
1174
|
APIConnectionError, # could not reach the server
|