boxd 0.2.19.dev901__tar.gz → 0.2.19.dev910__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.19.dev901/src/boxd.egg-info → boxd-0.2.19.dev910}/PKG-INFO +101 -31
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/README.md +99 -30
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/pyproject.toml +5 -1
- boxd-0.2.19.dev910/src/boxd/_generated/api_pb2.py +626 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_mappers.py +6 -1
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_requests.py +16 -3
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/errors.py +5 -1
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/models.py +29 -9
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/machines.py +136 -28
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/vars.py +4 -3
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/spec.py +36 -9
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910/src/boxd.egg-info}/PKG-INFO +101 -31
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd.egg-info/SOURCES.txt +1 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd.egg-info/requires.txt +1 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_mappers.py +5 -0
- boxd-0.2.19.dev910/tests/test_python_versions.py +115 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_spec.py +170 -2
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_transport.py +4 -0
- boxd-0.2.19.dev901/src/boxd/_generated/api_pb2.py +0 -626
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/LICENSE +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/setup.cfg +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/__init__.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_client.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_credentials.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_generated/api_pb2_grpc.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_transport.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_urls.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/_version_check.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/domains.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/orgs.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/snapshots.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd/resources/teams.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_credentials.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_delete_self.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_namespaces.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_requests.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_streaming.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/tests/test_urls.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.19.dev910}/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.19.
|
|
3
|
+
Version: 0.2.19.dev910
|
|
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"
|
|
@@ -265,7 +266,6 @@ engineering = (
|
|
|
265
266
|
.network("eng")
|
|
266
267
|
.label("team", "eng")
|
|
267
268
|
.env("NODE_ENV", "development")
|
|
268
|
-
.egress("github.com", "*.atlassian.net", "*.ubuntu.com")
|
|
269
269
|
.packages("apt-get update", "apt-get install -y nginx")
|
|
270
270
|
.with_(github(shared="eng-bot"), claude())
|
|
271
271
|
)
|
|
@@ -332,6 +332,14 @@ the org's secrets count. Only org owners and admins may list an org
|
|
|
332
332
|
shared machines, so a private machine can't list one. A name that resolves to
|
|
333
333
|
nothing, or that the create may not list, fails the create, naming it.
|
|
334
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
|
+
|
|
335
343
|
On the machine, the spec's `env` and its `secrets` win over your `private`
|
|
336
344
|
vars, which win over the org's `shared` vars (shared machines only), which win
|
|
337
345
|
over the org's `all` vars. A name can't be in both `env` and `secrets`.
|
|
@@ -366,6 +374,12 @@ with an org API key or from inside a machine, for someone who isn't a member,
|
|
|
366
374
|
and with a snapshot or disk the member couldn't use. Your own id is a plain
|
|
367
375
|
create. `assignTo` is never inherited: forks, snapshots and restores drop it.
|
|
368
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
|
+
|
|
369
383
|
#### Integrations the machine needs
|
|
370
384
|
|
|
371
385
|
A spec's `connections` and `agents` are the machine's requirements. Reading the
|
|
@@ -398,10 +412,17 @@ can't connect (`spec.connections.<toolkit>: ...`), and the GitHub App
|
|
|
398
412
|
|
|
399
413
|
`packages` are shell commands the machine runs once, in order, after its first
|
|
400
414
|
boot: as root, with bash (sh on an image without it),
|
|
401
|
-
`DEBIAN_FRONTEND=noninteractive`, no stdin
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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:
|
|
405
426
|
|
|
406
427
|
```python
|
|
407
428
|
m = boxd.machines.get("mike-dev")
|
|
@@ -410,18 +431,44 @@ for p in m.packages:
|
|
|
410
431
|
print(p.state, p.exit_code, p.command)
|
|
411
432
|
```
|
|
412
433
|
|
|
413
|
-
`state` is `"pending"`, `"running"`, `"done"` or `"
|
|
414
|
-
after a failure stay `"pending"`.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
420
459
|
hibernate while a command runs. At most 50 commands (a restore counts the
|
|
421
460
|
snapshot's too), each at most 4 KiB; the server names the one it refuses
|
|
422
461
|
(`spec.packages[2]: ...`). Commands that download something need the hosts in
|
|
423
462
|
the spec's `egress`, if it has one.
|
|
424
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
|
+
|
|
425
472
|
#### Updating a machine's spec
|
|
426
473
|
|
|
427
474
|
`update_spec` changes the spec of a machine created from one and applies what
|
|
@@ -451,23 +498,33 @@ Each change in `changes` says how it applies (`apply`):
|
|
|
451
498
|
|
|
452
499
|
| Field | `apply` |
|
|
453
500
|
|---|---|
|
|
454
|
-
| `labels`, `networks`, `egress` | `"live"` |
|
|
501
|
+
| `labels`, `networks`, `egress` | `"live"` (new egress rules are in force within about a second of the call returning) |
|
|
455
502
|
| `connections`, `agents` | `"live"`: what the machine needs (`requirements`) changes |
|
|
456
|
-
| `packages` | `"live"`: added commands run now, after the ones before them. Removing one undoes nothing (a warning says so); a command that already ran isn't run again; commands after a failed one don't run (a warning says so) |
|
|
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 |
|
|
457
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`) |
|
|
458
505
|
| `size` | `"reboot"` on a running machine, which reboots to apply it (as `resize`), so it needs `reboot=True`; `"live"` on a stopped one |
|
|
459
506
|
| `base`, `isolated`, `assignTo` | `"refused"`: fixed when the machine is created |
|
|
460
507
|
|
|
461
|
-
A refused change (also: new packages on a fork, a size change in standby or
|
|
508
|
+
A refused change (also: new packages or a retry on a fork, a size change in standby or
|
|
462
509
|
hibernated) refuses the whole patch, and so does a reboot without
|
|
463
|
-
`reboot=True`; nothing is applied.
|
|
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,
|
|
464
518
|
networks, egress and secrets it drives change together, then the size, then
|
|
465
|
-
the new packages run.
|
|
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
|
|
466
522
|
unknown toolkit, a secret you may not list, more than 50 commands in all are
|
|
467
523
|
refused, naming the field). `machine.created_spec` never changes, so the
|
|
468
524
|
console shows the spec badge as "updated". Allowed to whoever may edit the
|
|
469
525
|
machine's labels: its owner, or an org owner or admin on a shared machine.
|
|
470
|
-
The builder form only adds: a `size` that differs from the spec's raises
|
|
526
|
+
The builder form only adds: a `size` that differs from the spec's raises
|
|
527
|
+
(`"2X8"` is the same as `"2x8"`), so
|
|
471
528
|
use a patch to replace or remove. `spec_patch(from_spec, to_spec)` computes the
|
|
472
529
|
patch the builder form sends. `AsyncBoxd` has the same `await
|
|
473
530
|
boxd.machines.update_spec(...)`.
|
|
@@ -482,12 +539,14 @@ boxd.machines.get("alpha").egress_allow
|
|
|
482
539
|
boxd.machines.set_egress_allow("alpha", []) # clear: unrestricted again
|
|
483
540
|
```
|
|
484
541
|
|
|
485
|
-
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
|
|
486
545
|
requests are admitted by hostname, everything else by address, with the
|
|
487
546
|
addresses of allowlisted names learned as the machine resolves them. Nothing
|
|
488
547
|
that is not named is reachable. The hosts of any bound secret the machine
|
|
489
|
-
holds are always admitted. Takes effect within a second
|
|
490
|
-
|
|
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
|
|
491
550
|
machine restored from a snapshot starts unrestricted, unless its machine spec
|
|
492
551
|
lists `egress`.
|
|
493
552
|
|
|
@@ -576,6 +635,7 @@ machine.requirements # list[MachineRequirement] — the spec's connec
|
|
|
576
635
|
# kind, name, scope ("personal"/"shared"), tag, met, can_fix, reason
|
|
577
636
|
machine.packages # list[MachinePackage] — the spec's packages and how each went:
|
|
578
637
|
# command, state, exit_code, output_tail, reason, started_at, finished_at
|
|
638
|
+
# (output_tail is empty from list(): get() has it)
|
|
579
639
|
```
|
|
580
640
|
|
|
581
641
|
`None` always means "not set": a port that was never allocated, a boot that
|
|
@@ -794,18 +854,28 @@ boxd.secrets.delete("API_TOKEN", scope="shared")
|
|
|
794
854
|
```
|
|
795
855
|
|
|
796
856
|
A secret can be bound to the hosts it may be sent to. The machine then only
|
|
797
|
-
ever holds a placeholder, and the real value is substituted into
|
|
798
|
-
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:
|
|
799
859
|
|
|
800
860
|
```python
|
|
801
861
|
boxd.secrets.set("STRIPE_KEY", "sk_live_...", scope="all", domains=["api.stripe.com"])
|
|
802
862
|
```
|
|
803
863
|
|
|
804
|
-
Inside the machine, `STRIPE_KEY` is an opaque `bxds_…` string.
|
|
805
|
-
`api.stripe.com` carrying it
|
|
806
|
-
arrives at Stripe with the real key
|
|
807
|
-
|
|
808
|
-
|
|
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:
|
|
809
879
|
setting the secret again without it makes it a plain secret. Wildcards such as
|
|
810
880
|
`*.stripe.com` are accepted; provider-wide ones such as `*.amazonaws.com` are
|
|
811
881
|
refused.
|
|
@@ -826,7 +896,7 @@ Scope decides which machines a name applies to:
|
|
|
826
896
|
| `private` | only your machines in that organization |
|
|
827
897
|
| `shared` | the organization's shared machines |
|
|
828
898
|
| `all` | every machine in the organization |
|
|
829
|
-
| `unattached` | secrets only: just the machines whose spec lists it ([Secrets](#secrets)); the organization's with `org
|
|
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 |
|
|
830
900
|
| `private-unattached` | secrets only: your own unattached secret in the organization named by `org` |
|
|
831
901
|
|
|
832
902
|
`list` reports the organization's unattached secrets as `unattached` and your
|
|
@@ -1098,7 +1168,7 @@ from boxd import (
|
|
|
1098
1168
|
AuthenticationError, # no usable credential, or it was rejected
|
|
1099
1169
|
PermissionDeniedError, # authenticated, but not allowed
|
|
1100
1170
|
NotFoundError,
|
|
1101
|
-
ConflictError, # already exists,
|
|
1171
|
+
ConflictError, # already exists, fights the current state, or it changed meanwhile
|
|
1102
1172
|
RateLimitError, # rate limit or quota
|
|
1103
1173
|
APIStatusError, # any other error from the server
|
|
1104
1174
|
APIConnectionError, # could not reach the server
|
|
@@ -231,7 +231,6 @@ engineering = (
|
|
|
231
231
|
.network("eng")
|
|
232
232
|
.label("team", "eng")
|
|
233
233
|
.env("NODE_ENV", "development")
|
|
234
|
-
.egress("github.com", "*.atlassian.net", "*.ubuntu.com")
|
|
235
234
|
.packages("apt-get update", "apt-get install -y nginx")
|
|
236
235
|
.with_(github(shared="eng-bot"), claude())
|
|
237
236
|
)
|
|
@@ -298,6 +297,14 @@ the org's secrets count. Only org owners and admins may list an org
|
|
|
298
297
|
shared machines, so a private machine can't list one. A name that resolves to
|
|
299
298
|
nothing, or that the create may not list, fails the create, naming it.
|
|
300
299
|
|
|
300
|
+
Only org owners and admins may set, move or delete an org `unattached` secret;
|
|
301
|
+
your own (`private-unattached`) are yours. From inside a machine (with its
|
|
302
|
+
in-VM credentials) a spec can't list an unattached secret, yours or the org's,
|
|
303
|
+
on any machine, and `update_spec` can't change that machine's own `egress` or
|
|
304
|
+
`secrets`; make those changes from outside the machine, with an API key or in
|
|
305
|
+
the console. Names a fork or a restore takes over from its source's spec are
|
|
306
|
+
not affected.
|
|
307
|
+
|
|
301
308
|
On the machine, the spec's `env` and its `secrets` win over your `private`
|
|
302
309
|
vars, which win over the org's `shared` vars (shared machines only), which win
|
|
303
310
|
over the org's `all` vars. A name can't be in both `env` and `secrets`.
|
|
@@ -332,6 +339,12 @@ with an org API key or from inside a machine, for someone who isn't a member,
|
|
|
332
339
|
and with a snapshot or disk the member couldn't use. Your own id is a plain
|
|
333
340
|
create. `assignTo` is never inherited: forks, snapshots and restores drop it.
|
|
334
341
|
|
|
342
|
+
The machine runs the spec you wrote as the member's own machine: its packages
|
|
343
|
+
as root, its env and its base, alongside the member's GitHub token, agent
|
|
344
|
+
logins and personal connections. Assign a spec only if the member would run it
|
|
345
|
+
themselves. The member sees who created it (`created_by`) and can read the
|
|
346
|
+
spec (`machine.spec`).
|
|
347
|
+
|
|
335
348
|
#### Integrations the machine needs
|
|
336
349
|
|
|
337
350
|
A spec's `connections` and `agents` are the machine's requirements. Reading the
|
|
@@ -364,10 +377,17 @@ can't connect (`spec.connections.<toolkit>: ...`), and the GitHub App
|
|
|
364
377
|
|
|
365
378
|
`packages` are shell commands the machine runs once, in order, after its first
|
|
366
379
|
boot: as root, with bash (sh on an image without it),
|
|
367
|
-
`DEBIAN_FRONTEND=noninteractive`, no stdin
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
380
|
+
`DEBIAN_FRONTEND=noninteractive`, no stdin, the env and secrets an exec
|
|
381
|
+
session on the machine gets, and a 30-minute timeout each. Their output is
|
|
382
|
+
appended to `/var/log/boxd/packages.log` in the machine, which only root can
|
|
383
|
+
read. A command is done when its shell exits, so one that leaves a process
|
|
384
|
+
running in the background (`my-server &`) doesn't hold up the next. The first
|
|
385
|
+
failure (a non-zero exit, a timeout) stops the rest; the machine stays usable,
|
|
386
|
+
and `update_spec(id, {}, retry_packages=True)` runs the failed command and the ones after it again (so does
|
|
387
|
+
Retry in the console's Packages section). To give up on a command instead, or
|
|
388
|
+
to replace a wrong one, remove it from the spec: a command that leaves the spec
|
|
389
|
+
before it finished is skipped, and the ones after it run. Reading the machine
|
|
390
|
+
tells you how each one went:
|
|
371
391
|
|
|
372
392
|
```python
|
|
373
393
|
m = boxd.machines.get("mike-dev")
|
|
@@ -376,18 +396,44 @@ for p in m.packages:
|
|
|
376
396
|
print(p.state, p.exit_code, p.command)
|
|
377
397
|
```
|
|
378
398
|
|
|
379
|
-
`state` is `"pending"`, `"running"`, `"done"` or `"
|
|
380
|
-
after a failure stay `"pending"`.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
399
|
+
`state` is `"pending"`, `"running"`, `"done"`, `"failed"` or `"skipped"`; the
|
|
400
|
+
commands after a failure stay `"pending"`. A pending or failed command that an
|
|
401
|
+
`update_spec` removes from the spec becomes `"skipped"`: it never runs and no
|
|
402
|
+
longer stops the ones after it, which run right away (or when the machine next
|
|
403
|
+
runs). A skipped one that failed keeps its `exit_code` and `output_tail`, and
|
|
404
|
+
`reason` says when it was skipped. Listing it in the spec again puts it back to
|
|
405
|
+
`"pending"`, in its place. So replacing a broken command is one patch: drop it
|
|
406
|
+
and add the corrected one, and the corrected one runs, with everything that
|
|
407
|
+
was waiting. A running command can't be removed (the patch is refused until it
|
|
408
|
+
ends), and removing a done one undoes nothing. `output_tail` is the last 4 KiB of the
|
|
409
|
+
command's output, with the value of every secret the machine gets shown as
|
|
410
|
+
`[secret NAME]` (only the value as it is: printed encoded or in pieces, it isn't
|
|
411
|
+
recognised). The log file isn't masked: it is root's, and root on the machine
|
|
412
|
+
can read its env anyway. `output_tail` is empty on the machines `list()` returns,
|
|
413
|
+
which leave it out to keep the list small; `get()` has it. `reason` says why a failed one has no exit code (it
|
|
414
|
+
timed out, or was interrupted too often), or why a pending one is waiting. A
|
|
415
|
+
command the platform interrupts (the machine stops, reboots, hibernates or
|
|
416
|
+
moves, or boxd restarts on the machine's host) goes back to `"pending"` and runs
|
|
417
|
+
again from the start once the machine is running, and the rest follow; after 3
|
|
418
|
+
interruptions it fails. What finished never runs again: not on a reboot, a
|
|
419
|
+
resume, a wake or a move. Restoring a snapshot marks done the commands that had
|
|
420
|
+
run on the machine the snapshot was taken from and runs the snapshot spec's
|
|
421
|
+
others (one that was running when the snapshot was taken runs again from the
|
|
422
|
+
start), then the ones the restore's spec adds; a fork keeps its source's states
|
|
423
|
+
and runs nothing. The machine doesn't auto-suspend or
|
|
386
424
|
hibernate while a command runs. At most 50 commands (a restore counts the
|
|
387
425
|
snapshot's too), each at most 4 KiB; the server names the one it refuses
|
|
388
426
|
(`spec.packages[2]: ...`). Commands that download something need the hosts in
|
|
389
427
|
the spec's `egress`, if it has one.
|
|
390
428
|
|
|
429
|
+
A spec has size limits, checked before anything else and named with what was
|
|
430
|
+
given (`spec.env: at most 128 variables (got 129)`): at most 256 KiB as JSON
|
|
431
|
+
in all; `env` at most 128 variables, each value at most 32 KiB and all values
|
|
432
|
+
together at most 64 KiB; `secrets` at most 64 names; `networks` at most 16, each
|
|
433
|
+
name at most 63 bytes; `egress` at most 256 entries, none empty (leave `egress`
|
|
434
|
+
out for no allowlist); `connections` at most 64; `packages` at most 200 listed
|
|
435
|
+
and 50 once duplicates are dropped. An `update_spec` patch is at most 256 KiB.
|
|
436
|
+
|
|
391
437
|
#### Updating a machine's spec
|
|
392
438
|
|
|
393
439
|
`update_spec` changes the spec of a machine created from one and applies what
|
|
@@ -417,23 +463,33 @@ Each change in `changes` says how it applies (`apply`):
|
|
|
417
463
|
|
|
418
464
|
| Field | `apply` |
|
|
419
465
|
|---|---|
|
|
420
|
-
| `labels`, `networks`, `egress` | `"live"` |
|
|
466
|
+
| `labels`, `networks`, `egress` | `"live"` (new egress rules are in force within about a second of the call returning) |
|
|
421
467
|
| `connections`, `agents` | `"live"`: what the machine needs (`requirements`) changes |
|
|
422
|
-
| `packages` | `"live"`: added commands run now, after the ones before them. Removing one undoes nothing (a warning says so); a command that already ran isn't run again; commands after a failed one don't run (a warning says so) |
|
|
468
|
+
| `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 |
|
|
423
469
|
| `env`, `secrets` | `"reboot_recommended"`: new shells and exec sessions get them now; processes already running keep the old values until they restart (`reboot_recommended`) |
|
|
424
470
|
| `size` | `"reboot"` on a running machine, which reboots to apply it (as `resize`), so it needs `reboot=True`; `"live"` on a stopped one |
|
|
425
471
|
| `base`, `isolated`, `assignTo` | `"refused"`: fixed when the machine is created |
|
|
426
472
|
|
|
427
|
-
A refused change (also: new packages on a fork, a size change in standby or
|
|
473
|
+
A refused change (also: new packages or a retry on a fork, a size change in standby or
|
|
428
474
|
hibernated) refuses the whole patch, and so does a reboot without
|
|
429
|
-
`reboot=True`; nothing is applied.
|
|
475
|
+
`reboot=True`; nothing is applied. So is an update during which the spec
|
|
476
|
+
changed (another update, a label edit): it raises `ConflictError`, and
|
|
477
|
+
reading the machine again and retrying is safe. The builder form does that
|
|
478
|
+
itself: it sends the spec it read, so a change made in between can't be
|
|
479
|
+
undone by its lists, and on a conflict reads the spec again and reapplies the
|
|
480
|
+
function, up to 3 times. A patch you built from a spec you read can say so
|
|
481
|
+
too, `expected_spec=machine.spec`: if the spec is no longer that one, nothing
|
|
482
|
+
is applied and it raises `ConflictError`. Otherwise the spec and the labels,
|
|
430
483
|
networks, egress and secrets it drives change together, then the size, then
|
|
431
|
-
the new packages run.
|
|
484
|
+
the new packages run. If the size then fails (the org's quota changed, the
|
|
485
|
+
machine went to standby), the rest stays applied and the call still returns:
|
|
486
|
+
`resize_error` says why, and `spec` has the size the machine has. The patched spec is checked like a create's (an
|
|
432
487
|
unknown toolkit, a secret you may not list, more than 50 commands in all are
|
|
433
488
|
refused, naming the field). `machine.created_spec` never changes, so the
|
|
434
489
|
console shows the spec badge as "updated". Allowed to whoever may edit the
|
|
435
490
|
machine's labels: its owner, or an org owner or admin on a shared machine.
|
|
436
|
-
The builder form only adds: a `size` that differs from the spec's raises
|
|
491
|
+
The builder form only adds: a `size` that differs from the spec's raises
|
|
492
|
+
(`"2X8"` is the same as `"2x8"`), so
|
|
437
493
|
use a patch to replace or remove. `spec_patch(from_spec, to_spec)` computes the
|
|
438
494
|
patch the builder form sends. `AsyncBoxd` has the same `await
|
|
439
495
|
boxd.machines.update_spec(...)`.
|
|
@@ -448,12 +504,14 @@ boxd.machines.get("alpha").egress_allow
|
|
|
448
504
|
boxd.machines.set_egress_allow("alpha", []) # clear: unrestricted again
|
|
449
505
|
```
|
|
450
506
|
|
|
451
|
-
Entries are hostnames or wildcards, or public IPv4 addresses and CIDRs
|
|
507
|
+
Entries are hostnames or wildcards, or public IPv4 addresses and CIDRs, at
|
|
508
|
+
most 256; an internationalized name is kept in its ASCII form (`bücher.de`
|
|
509
|
+
becomes `xn--bcher-kva.de`), which is what the machine looks up. Web
|
|
452
510
|
requests are admitted by hostname, everything else by address, with the
|
|
453
511
|
addresses of allowlisted names learned as the machine resolves them. Nothing
|
|
454
512
|
that is not named is reachable. The hosts of any bound secret the machine
|
|
455
|
-
holds are always admitted. Takes effect within a second
|
|
456
|
-
|
|
513
|
+
holds are always admitted. Takes effect within a second. From inside the
|
|
514
|
+
machine it can be read (`boxd machine egress`) but not changed. A fork inherits its source's allowlist; a
|
|
457
515
|
machine restored from a snapshot starts unrestricted, unless its machine spec
|
|
458
516
|
lists `egress`.
|
|
459
517
|
|
|
@@ -542,6 +600,7 @@ machine.requirements # list[MachineRequirement] — the spec's connec
|
|
|
542
600
|
# kind, name, scope ("personal"/"shared"), tag, met, can_fix, reason
|
|
543
601
|
machine.packages # list[MachinePackage] — the spec's packages and how each went:
|
|
544
602
|
# command, state, exit_code, output_tail, reason, started_at, finished_at
|
|
603
|
+
# (output_tail is empty from list(): get() has it)
|
|
545
604
|
```
|
|
546
605
|
|
|
547
606
|
`None` always means "not set": a port that was never allocated, a boot that
|
|
@@ -760,18 +819,28 @@ boxd.secrets.delete("API_TOKEN", scope="shared")
|
|
|
760
819
|
```
|
|
761
820
|
|
|
762
821
|
A secret can be bound to the hosts it may be sent to. The machine then only
|
|
763
|
-
ever holds a placeholder, and the real value is substituted into
|
|
764
|
-
those hosts on the way out:
|
|
822
|
+
ever holds a placeholder, and the real value is substituted into HTTPS
|
|
823
|
+
requests to those hosts on the way out:
|
|
765
824
|
|
|
766
825
|
```python
|
|
767
826
|
boxd.secrets.set("STRIPE_KEY", "sk_live_...", scope="all", domains=["api.stripe.com"])
|
|
768
827
|
```
|
|
769
828
|
|
|
770
|
-
Inside the machine, `STRIPE_KEY` is an opaque `bxds_…` string.
|
|
771
|
-
`api.stripe.com` carrying it
|
|
772
|
-
arrives at Stripe with the real key
|
|
773
|
-
|
|
774
|
-
|
|
829
|
+
Inside the machine, `STRIPE_KEY` is an opaque `bxds_…` string. An HTTPS
|
|
830
|
+
request to `api.stripe.com` carrying it anywhere (the URL, a header, Basic auth
|
|
831
|
+
included, or the body) arrives at Stripe with the real key. It is replaced only
|
|
832
|
+
over HTTPS, and only when both the server name the connection was opened for
|
|
833
|
+
and the request's own `Host` are hosts the secret is bound to: over plain HTTP
|
|
834
|
+
and in a request to any other host it stays the useless placeholder. A body
|
|
835
|
+
over 16 MiB with a `Content-Length` is sent as written, placeholder included.
|
|
836
|
+
Responses on those connections carry the placeholder instead of the value:
|
|
837
|
+
wherever the real value (8 characters or more) appears in a response's headers
|
|
838
|
+
or body exactly as it was sent, it is replaced, so a server that echoes the
|
|
839
|
+
request doesn't hand the value to the machine. A value the server sends back
|
|
840
|
+
re-encoded (base64, URL- or JSON-escaped) isn't caught, and neither is one in
|
|
841
|
+
a body compressed with anything but gzip or deflate (the proxy asks for an
|
|
842
|
+
uncompressed response). Nothing to configure in the machine, and the
|
|
843
|
+
placeholder does not change when the value is rotated. `domains` is the full desired set:
|
|
775
844
|
setting the secret again without it makes it a plain secret. Wildcards such as
|
|
776
845
|
`*.stripe.com` are accepted; provider-wide ones such as `*.amazonaws.com` are
|
|
777
846
|
refused.
|
|
@@ -792,7 +861,7 @@ Scope decides which machines a name applies to:
|
|
|
792
861
|
| `private` | only your machines in that organization |
|
|
793
862
|
| `shared` | the organization's shared machines |
|
|
794
863
|
| `all` | every machine in the organization |
|
|
795
|
-
| `unattached` | secrets only: just the machines whose spec lists it ([Secrets](#secrets)); the organization's with `org
|
|
864
|
+
| `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 |
|
|
796
865
|
| `private-unattached` | secrets only: your own unattached secret in the organization named by `org` |
|
|
797
866
|
|
|
798
867
|
`list` reports the organization's unattached secrets as `unattached` and your
|
|
@@ -1064,7 +1133,7 @@ from boxd import (
|
|
|
1064
1133
|
AuthenticationError, # no usable credential, or it was rejected
|
|
1065
1134
|
PermissionDeniedError, # authenticated, but not allowed
|
|
1066
1135
|
NotFoundError,
|
|
1067
|
-
ConflictError, # already exists,
|
|
1136
|
+
ConflictError, # already exists, fights the current state, or it changed meanwhile
|
|
1068
1137
|
RateLimitError, # rate limit or quota
|
|
1069
1138
|
APIStatusError, # any other error from the server
|
|
1070
1139
|
APIConnectionError, # could not reach the server
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boxd"
|
|
3
|
-
version = "0.2.19.
|
|
3
|
+
version = "0.2.19.dev910"
|
|
4
4
|
description = "Python SDK for the boxd cloud VM platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -30,6 +30,10 @@ dependencies = [
|
|
|
30
30
|
"grpcio>=1.80",
|
|
31
31
|
"protobuf>=6.31.1",
|
|
32
32
|
"pydantic>=2",
|
|
33
|
+
# spec.py takes TypedDict from here, not typing: pydantic refuses
|
|
34
|
+
# typing.TypedDict as a field type below Python 3.12. 4.12 is also
|
|
35
|
+
# what grpcio 1.80 needs.
|
|
36
|
+
"typing-extensions>=4.12",
|
|
33
37
|
# Cap below 1.0: httpx 1.0 (currently in pre-release as 1.0.devN)
|
|
34
38
|
# removed `httpx.AsyncClient`, which _credentials.py uses for the
|
|
35
39
|
# API-key → JWT exchange. Without this cap, anyone running `pip
|