boxd 0.2.18.dev900__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.
Files changed (47) hide show
  1. {boxd-0.2.18.dev900/src/boxd.egg-info → boxd-0.2.19.dev910}/PKG-INFO +101 -31
  2. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/README.md +99 -30
  3. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/pyproject.toml +5 -1
  4. boxd-0.2.19.dev910/src/boxd/_generated/api_pb2.py +626 -0
  5. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_mappers.py +6 -1
  6. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_requests.py +16 -3
  7. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/errors.py +5 -1
  8. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/models.py +29 -9
  9. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/machines.py +136 -28
  10. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/vars.py +4 -3
  11. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/spec.py +36 -9
  12. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910/src/boxd.egg-info}/PKG-INFO +101 -31
  13. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd.egg-info/SOURCES.txt +1 -0
  14. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd.egg-info/requires.txt +1 -0
  15. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_mappers.py +5 -0
  16. boxd-0.2.19.dev910/tests/test_python_versions.py +115 -0
  17. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_spec.py +170 -2
  18. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_transport.py +4 -0
  19. boxd-0.2.18.dev900/src/boxd/_generated/api_pb2.py +0 -626
  20. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/LICENSE +0 -0
  21. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/setup.cfg +0 -0
  22. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/__init__.py +0 -0
  23. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_client.py +0 -0
  24. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_credentials.py +0 -0
  25. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_generated/__init__.py +0 -0
  26. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_generated/api_pb2_grpc.py +0 -0
  27. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_streaming.py +0 -0
  28. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_transport.py +0 -0
  29. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_urls.py +0 -0
  30. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/_version_check.py +0 -0
  31. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/__init__.py +0 -0
  32. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/account.py +0 -0
  33. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/credentials.py +0 -0
  34. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/disks.py +0 -0
  35. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/domains.py +0 -0
  36. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/orgs.py +0 -0
  37. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/snapshots.py +0 -0
  38. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd/resources/teams.py +0 -0
  39. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd.egg-info/dependency_links.txt +0 -0
  40. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/src/boxd.egg-info/top_level.txt +0 -0
  41. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_credentials.py +0 -0
  42. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_delete_self.py +0 -0
  43. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_namespaces.py +0 -0
  44. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_requests.py +0 -0
  45. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_streaming.py +0 -0
  46. {boxd-0.2.18.dev900 → boxd-0.2.19.dev910}/tests/test_urls.py +0 -0
  47. {boxd-0.2.18.dev900 → 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.18.dev900
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 and a 30-minute timeout each. Their
402
- output is appended to `/var/log/boxd/packages.log` in the machine. The first
403
- failure (a non-zero exit, a timeout) stops the rest; the machine stays usable.
404
- Reading the machine tells you how each one went:
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 `"failed"`; the commands
414
- after a failure stay `"pending"`. `output_tail` is the last 4 KiB of the
415
- command's output, and `reason` says why a failed one has no exit code (it
416
- timed out, or the run was interrupted). Nothing runs again: not on a reboot, a
417
- resume, a wake or a move. Restoring a snapshot whose spec lists packages marks
418
- those done and runs only the ones the restore's spec adds; a fork keeps its
419
- source's states and runs nothing. The machine doesn't auto-suspend or
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. Otherwise the spec and the labels,
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. The patched spec is checked like a create's (an
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, so
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. Web
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 and is not visible or
490
- changeable from inside the machine. A fork inherits its source's allowlist; a
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 requests to
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. A request to
805
- `api.stripe.com` carrying it, in a header, the query, the body or Basic auth,
806
- arrives at Stripe with the real key; a request anywhere else carries the
807
- useless placeholder. Nothing to configure in the machine, and the placeholder
808
- does not change when the value is rotated. `domains` is the full desired set:
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`, else your own |
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, or fights the current state
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 and a 30-minute timeout each. Their
368
- output is appended to `/var/log/boxd/packages.log` in the machine. The first
369
- failure (a non-zero exit, a timeout) stops the rest; the machine stays usable.
370
- Reading the machine tells you how each one went:
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 `"failed"`; the commands
380
- after a failure stay `"pending"`. `output_tail` is the last 4 KiB of the
381
- command's output, and `reason` says why a failed one has no exit code (it
382
- timed out, or the run was interrupted). Nothing runs again: not on a reboot, a
383
- resume, a wake or a move. Restoring a snapshot whose spec lists packages marks
384
- those done and runs only the ones the restore's spec adds; a fork keeps its
385
- source's states and runs nothing. The machine doesn't auto-suspend or
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. Otherwise the spec and the labels,
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. The patched spec is checked like a create's (an
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, so
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. Web
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 and is not visible or
456
- changeable from inside the machine. A fork inherits its source's allowlist; a
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 requests to
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. A request to
771
- `api.stripe.com` carrying it, in a header, the query, the body or Basic auth,
772
- arrives at Stripe with the real key; a request anywhere else carries the
773
- useless placeholder. Nothing to configure in the machine, and the placeholder
774
- does not change when the value is rotated. `domains` is the full desired set:
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`, else your own |
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, or fights the current state
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.18.dev900"
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