cf-orbs 0.0.0-stage → 0.1.0

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 (57) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +275 -2
  4. package/bin/cf-orbs.js +14 -0
  5. package/dist/api.js +124 -0
  6. package/dist/args.js +77 -0
  7. package/dist/commands/bench.js +6 -0
  8. package/dist/commands/deploy.js +183 -0
  9. package/dist/commands/doctor.js +159 -0
  10. package/dist/commands/init.js +466 -0
  11. package/dist/commands/logs.js +42 -0
  12. package/dist/commands/snapshots.js +306 -0
  13. package/dist/commands/status.js +215 -0
  14. package/dist/commands/uninstall.js +96 -0
  15. package/dist/commands/upgrade.js +108 -0
  16. package/dist/config.js +460 -0
  17. package/dist/connect.js +19 -0
  18. package/dist/context.js +16 -0
  19. package/dist/errors.js +35 -0
  20. package/dist/index.js +197 -0
  21. package/dist/probes.js +21 -0
  22. package/dist/project.js +215 -0
  23. package/dist/subdomain.js +81 -0
  24. package/dist/ui.js +113 -0
  25. package/dist/wrangler-template.js +69 -0
  26. package/dist/wrangler.js +169 -0
  27. package/package.json +40 -4
  28. package/templates/cf-orbs.config.example.jsonc +56 -0
  29. package/templates/dispatch/README.md +69 -0
  30. package/templates/dispatch/dispatch.sh +438 -0
  31. package/templates/image/Dockerfile +34 -0
  32. package/templates/image/README.md +83 -0
  33. package/templates/provider/Dockerfile +25 -0
  34. package/templates/provider/README.md +134 -0
  35. package/templates/provider/entry.sh +116 -0
  36. package/templates/worker/src/api.ts +532 -0
  37. package/templates/worker/src/core/config.ts +149 -0
  38. package/templates/worker/src/core/container-port.ts +137 -0
  39. package/templates/worker/src/core/registry-core.ts +635 -0
  40. package/templates/worker/src/core/types.ts +263 -0
  41. package/templates/worker/src/env.ts +26 -0
  42. package/templates/worker/src/index.ts +29 -0
  43. package/templates/worker/src/orb/alarm.ts +318 -0
  44. package/templates/worker/src/orb/bench.ts +129 -0
  45. package/templates/worker/src/orb/context.ts +16 -0
  46. package/templates/worker/src/orb/delete.ts +33 -0
  47. package/templates/worker/src/orb/exclusive.ts +22 -0
  48. package/templates/worker/src/orb/images.ts +47 -0
  49. package/templates/worker/src/orb/journal.ts +203 -0
  50. package/templates/worker/src/orb/pause.ts +269 -0
  51. package/templates/worker/src/orb/prewarm.ts +122 -0
  52. package/templates/worker/src/orb/scripts.ts +544 -0
  53. package/templates/worker/src/orb/session.ts +557 -0
  54. package/templates/worker/src/orb/spawn.ts +697 -0
  55. package/templates/worker/src/provider-restart.ts +73 -0
  56. package/templates/worker/src/provider.ts +487 -0
  57. package/templates/worker/src/registry.ts +136 -0
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,11 @@
1
+ cf-orbs
2
+ Copyright 2026 Meir Dick
3
+
4
+ This product includes software patterns adapted from cloudflare/sandbox-sdk
5
+ (Apache-2.0, https://github.com/cloudflare/sandbox-sdk): the setsid + boot_id
6
+ background-process guard, the per-object exclusive operation queue, and the
7
+ auto-save alarm loop.
8
+
9
+ cf-orbs is a community project. It is not affiliated with Cloudflare, Inc. or
10
+ with Sourcegraph, Inc. (Amp). "Cloudflare" and "Amp" are trademarks of their
11
+ respective owners.
package/README.md CHANGED
@@ -1,3 +1,276 @@
1
- # Temporary Holding Version
1
+ # cf-orbs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run [Amp](https://ampcode.com) orbs on your own Cloudflare account. cf-orbs is a self-hosted Amp orb provider built on Cloudflare Containers. You install it with one command.
4
+
5
+ Community project. Not affiliated with Cloudflare or Amp (Sourcegraph). "Cloudflare" and "Amp" are trademarks of their owners.
6
+
7
+ ## Why cf-orbs
8
+
9
+ Measured against Amp-hosted orbs and Amp's reference providers for Docker, E2B and Vercel, same prompt, same project, same night (2026-10-31, [full table](https://cf-orbs-site.mdick85.workers.dev/#compare), [method](https://github.com/meirdick/cf-orbs/blob/main/docs/benchmarks.md)):
10
+
11
+ - **Fast new threads.** Project snapshots skip `.agents/setup`: about 12 s of orb setup before the first turn, against 36-45 s for the E2B and Vercel reference scripts, which rebuild the repo every thread.
12
+ - **Fast wake.** Warm pause keeps the orb and its Amp executor alive for 10 min after Amp pauses it (snapshot taken first, `warmMinutes` to change). A follow-up in that window runs its first tool in 0.3-0.5 s; Amp-hosted takes 0.85 s, a cold restore 1.5-2 s plus process start.
13
+ - **Fast network.** git clone 0.26 s, npm install of express 7.7 s; Amp-hosted a1.small 0.54 s and 15.2 s; E2B 0.44 s and 10.1 s.
14
+ - **Same interactive speed.** Tool round trips on a live orb are 0.8-0.9 s on every provider.
15
+ - **Reliable.** 10 of 10 threads in every benchmark round. A lost orb comes back in 22 s and the agent retries. Paused orbs restore after an image rebuild.
16
+ - **Cheapest cloud option at this size.** About $0.11/h running idle and $0.17/h at 20% CPU for 4 vCPU / 12 GiB / 20 GB; no compute charge while paused (snapshot storage has no published rate yet). Amp-hosted a1.small is $0.17/h for 2 CPU / 4 GiB; E2B about $0.33/h for 4 vCPU / 8 GiB.
17
+ - **Yours.** One CLI, no cluster, no extra API keys. Orbs, snapshots and logs stay in your Cloudflare account.
18
+
19
+ Known limits, measured: writes of new data on the root disk are slower (512 MB with fsync 0.9 s with `hugePages`, was 1.3 s; Amp-hosted 0.6 s; use `/mnt/fast` for caches, see `fastTmpfsGb`); 4 vCPU / 12 GiB / 20 GB per orb; the first spawn on a new Cloudflare host pulls the 1.8 GB image (40-50 s, then cached). Details in [docs/05](https://github.com/meirdick/cf-orbs/blob/main/docs/benchmarks.md).
20
+
21
+ ## What it is
22
+
23
+ An Amp orb is the machine where an Amp agent runs its tools: it clones your repo, runs commands and edits files. Amp runs orbs on its own servers by default. With a self-hosted provider, the orbs run on your infrastructure instead.
24
+
25
+ cf-orbs makes each orb a Cloudflare Container: a microVM with its own kernel, network and disk, started from Amp's orb image. It has three parts, all in your Cloudflare account:
26
+
27
+ - **A Worker** (`cf-orbs`) with Durable Objects. It starts, pauses and deletes orbs and keeps track of their snapshots.
28
+ - **A provider container.** It runs `amp orb provider`, which takes orb jobs from Amp over an outbound connection.
29
+ - **The orbs.** One container per Amp thread.
30
+
31
+ When Amp pauses an orb, cf-orbs saves the disk as a snapshot and stops the container, so a paused orb has no compute charge. The next message restores the disk. Amp project snapshots work too: a new thread on a known repo starts from the disk that Amp already set up.
32
+
33
+ ## Prerequisites
34
+
35
+ | You need | Check |
36
+ | --- | --- |
37
+ | Node 22 or later (wrangler 4 needs it) | `node --version` |
38
+ | Docker, running. wrangler builds the orb image on your machine | `docker info` |
39
+ | A Cloudflare account on **Workers Paid** ($5/month; Containers need it) | Dashboard → Workers & Pages → Plans |
40
+ | wrangler logged in to that account | `npx wrangler login`, then `npx wrangler whoami` |
41
+ | An Amp provider token (`aop_live_…`) | Amp → Settings → Orbs → Add Provider. Note the provider name you type there |
42
+
43
+ A personal provider needs no workspace admin. Make it in your personal Amp settings.
44
+
45
+ ## Quick start
46
+
47
+ ```sh
48
+ mkdir my-orbs && cd my-orbs
49
+ npx cf-orbs init --provider-name <name from Amp>
50
+ ```
51
+
52
+ `init` asks for the Amp provider token. It does not show what you type, and the token does not go into your shell history.
53
+
54
+ Measured on 2026-10-30 in an empty directory: **4 min 41 s** from `init` to a running provider. That includes the push of the 1.8 GB orb image (the whole deploy took 247 s). A slow uplink takes longer. The Cloudflare registry sometimes times out on a large push, or answers `Unauthorized` after image preparation. `init` retries both automatically, up to 3 times, and keeps the layers that it already pushed.
55
+
56
+ What `init` does:
57
+
58
+ 1. Checks Node, Docker, the wrangler login and the account.
59
+ 2. Writes `cf-orbs.config.jsonc`, `image/Dockerfile` and `.cf-orbs/` (generated files, the control token and state).
60
+ 3. Registers a workers.dev subdomain if the account has none (it asks for a name, or takes `--subdomain`).
61
+ 4. Runs `wrangler deploy`: builds and pushes the orb image and the provider image, then deploys the Worker.
62
+ 5. Checks `/v1/health` and `/v1/status`.
63
+ 6. Starts the provider and waits until it runs for 15 s with a clean log.
64
+ 7. Prints a command to try:
65
+
66
+ ```sh
67
+ amp -ox "hello from cf-orbs" --orb-provider <name>
68
+ ```
69
+
70
+ The name after `--orb-provider` must match the provider name in Amp Settings. In the Amp app, pick the provider under **Self-Hosted** when you start a thread.
71
+
72
+ `init` is safe to run again. It keeps the control token, and it keeps the Amp token secret unless you give a new one.
73
+
74
+ Non-interactive (CI or an agent): put the token in a file with mode 600 and pass `--token-file`. In CI, `AMP_ORB_PROVIDER_TOKEN` from a secret store also works.
75
+
76
+ ```sh
77
+ npx cf-orbs init --non-interactive --token-file <file> --provider-name <name> --subdomain <name> \
78
+ --account-id <id> # only if wrangler sees more than one account
79
+ ```
80
+
81
+ Orb size: `init` writes 4 vCPU, 12 GiB, 20 GB disk. For a cheaper orb add `--vcpu 2` (2 vCPU, 6 GiB, about half the memory cost). `--vcpu <1|2|3|4>` writes N vCPU and N x 3 GiB. For any other size, edit `instance` in `cf-orbs.config.jsonc`.
82
+
83
+ An AI agent that installs cf-orbs for you can follow [AGENTS.md](https://github.com/meirdick/cf-orbs/blob/main/AGENTS.md).
84
+
85
+ ### From a clone
86
+
87
+ To run the CLI from source (for development, or to try a change):
88
+
89
+ ```sh
90
+ git clone https://github.com/meirdick/cf-orbs && cd cf-orbs
91
+ npm ci && npm run build -w cli
92
+ mkdir ../my-orbs
93
+ node cli/bin/cf-orbs.js init ../my-orbs --provider-name <name> # asks for the token
94
+ ```
95
+
96
+ Then use `node <repo>/cli/bin/cf-orbs.js` wherever this README says `cf-orbs`, with `--dir ../my-orbs` or from inside that directory.
97
+
98
+ ## What you get
99
+
100
+ - Orbs that run on your Cloudflare account. The repo checkout, build output and saved disks stay there.
101
+ - One orb size per install. Default: 4 vCPU, 12 GiB memory, 20 GB disk. Set it with `init --vcpu` or in `cf-orbs.config.jsonc`.
102
+ - Pause and resume through disk snapshots, and Amp project snapshots for fast new threads.
103
+ - A watchdog: it pauses an orb that stays awake longer than `maxAwakeHours` (default 12) and caps awake orbs at `maxRunning` (default 20).
104
+ - `status`, `logs` and `doctor` commands. Every failure is one line: `Failed: <cause> — fix: <what to do>`.
105
+
106
+ ## Pause and resume
107
+
108
+ Amp decides when an orb pauses: after the agent is idle and nobody interacts with the thread for about 20 minutes (we saw 20-25 min). An archived thread pauses too. A deleted thread deletes its orb.
109
+
110
+ On pause, cf-orbs stops the Amp processes, takes a snapshot of the disk and stops the container. On the next message, Amp sends a spawn and cf-orbs restores the disk. Measured: spawn from a snapshot in 2.3 s (container ready in 0.6 s).
111
+
112
+ | Kept | Lost |
113
+ | --- | --- |
114
+ | Files, the repo, installed packages, build caches (everything on the disk) | Running processes: dev servers, watchers, terminal sessions |
115
+ | | Memory |
116
+
117
+ Warm pause (`warmMinutes`, default 10): cf-orbs saves the disk at the pause but keeps the container and Amp's processes running for 10 more minutes. A message in that time reuses them: no restore, no restart. After that the orb stops. A warm orb counts toward `maxRunning` and is billed like an awake orb. `cf-orbs status` shows `warm` and the time left.
118
+
119
+ Amp runs `.agents/resume` on wake, and portal services restart when you open them. Amp-hosted orbs keep memory across a pause; cf-orbs does not.
120
+
121
+ Other things to know:
122
+
123
+ - A paused orb can wake when somebody views its thread, with no new message. It pauses again later.
124
+ - A snapshot is kept for 30 days after its last restore. An orb paused longer than that starts fresh, and Amp sets up the repo again.
125
+ - Each pause makes a new snapshot, and storage is billed. `cf-orbs snapshots prune` lists the old ones that no orb uses; `--yes` deletes them through `wrangler containers images delete` (works, but Cloudflare does not document it). `cf-orbs status` shows `prunable: N (X GB)`.
126
+ - Work done after the last pause can be lost if Cloudflare stops a container without warning. To limit that, set `checkpointMinutes` (15 or more) to snapshot awake orbs that have changes.
127
+ - The restore is lazy. The first read of a large build cache after a wake is slower than later reads (about 230 MB/s).
128
+ - A project snapshot goes stale after an Amp CLI release (each new thread then spends 4-5 s on the amp update) or after a `cf-orbs deploy` with a new image. With `projectSnapshotRefresh: "on-change"`, cf-orbs checks for this when a new thread starts from the snapshot. If it is stale, cf-orbs forgets the mapping (at most once per project per 24 h), so the next new thread boots the image and Amp runs setup.
129
+ - This is off by default because cf-orbs never starts threads or orbs for it, so nothing extra shows in your Amp UI. Also, Amp does not always save a new snapshot after a forget. Until it does, each new thread runs setup, and `cf-orbs status` shows the snapshot as stale.
130
+ - To do it by hand: delete the cached snapshot in Amp → Orb Settings → Snapshots, then run `cf-orbs snapshots forget --all-project`.
131
+
132
+ ## Costs
133
+
134
+ Rates: [Cloudflare Containers pricing](https://developers.cloudflare.com/containers/platform/pricing/) and [Amp orb pricing](https://ampcode.com/docs/orbs/sizes-and-costs). Measured speed and cost: [docs/benchmarks.md](https://github.com/meirdick/cf-orbs/blob/main/docs/benchmarks.md).
135
+
136
+ | Item | Cost (USD) |
137
+ | --- | --- |
138
+ | Workers Paid plan | $5.00/month |
139
+ | Provider container (`lite`, always on) | ~$2.01/month |
140
+ | Default orb (4 vCPU, 12 GiB, 20 GB), awake | $0.113/h fixed + CPU: ~$0.171/h at 20% CPU, ~$0.401/h at 100% |
141
+ | Orb with `--vcpu 2` (2 vCPU, 6 GiB, 20 GB), awake | $0.059/h fixed + CPU: ~$0.088/h at 20% CPU, ~$0.203/h at 100% |
142
+ | Paused orb | No compute charge. Snapshot storage price is not published yet (beta) |
143
+ | Egress | $0.025/GB in NA/EU after the first 1 TB |
144
+ | Amp fee | $0 on non-Enterprise workspaces. Enterprise workspaces pay Amp's self-hosted orb rate |
145
+
146
+ Example: 120 awake hours a month at 20% CPU costs about $27.50 a month in total on the default size, or $17.55 with `--vcpu 2`. Amp-hosted a1.small (2 vCPU, 4 GiB) is $20.40 and a1.medium (4 vCPU, 8 GiB) is $39.60 for the same hours. On an Enterprise workspace cf-orbs costs more than Amp-hosted orbs: the reason to use it there is that your code stays in your account, not price.
147
+
148
+ ## Commands
149
+
150
+ | Command | Does |
151
+ | --- | --- |
152
+ | `cf-orbs init [dir]` | Scaffold, deploy and check a provider. Flags: `--provider cloudflare\|external`, `--token-file`, `--account-id`, `--provider-name`, `--subdomain`, `--vcpu <1\|2\|3\|4>` |
153
+ | `cf-orbs status [orb]` | Install status (provider, awake orbs, snapshots), or one orb |
154
+ | `cf-orbs logs <orb>` | Redacted bootstrap log of one orb. `--lines N` (default 200) |
155
+ | `cf-orbs logs --provider` | Redacted provider log (last 20 lines) |
156
+ | `cf-orbs doctor` | PASS/FAIL per check, each with a one-line fix. Never starts anything |
157
+ | `cf-orbs deploy` | Regenerate `.cf-orbs/wrangler.jsonc` from the config and deploy |
158
+ | `cf-orbs snapshots list` | Project snapshot mappings: Amp snapshot id → Cloudflare snapshot |
159
+ | `cf-orbs snapshots forget <id>` / `forget --all-project` | Drop mappings. The next thread on that project runs setup again |
160
+ | `cf-orbs snapshots prune [--older-than 7d] [--all] [--yes]` | List snapshot data that no orb and no project mapping uses, unused for 7 days. `--yes` deletes it. `--all` removes only the age limit |
161
+ | `cf-orbs upgrade --digest sha256:…` | Move the orb image to a new Amp orb image digest. Keeps the old image key |
162
+ | `cf-orbs uninstall [--yes]` | List (dry run) or delete the Worker, container apps and images |
163
+
164
+ Every command takes `--json`, `--dir <project>` and `--non-interactive`. Orb ids are `amp-<thread id>`; `cf-orbs status` lists the awake ones.
165
+
166
+ ## Configuration
167
+
168
+ `cf-orbs.config.jsonc` in your project directory. Edit it, then run `cf-orbs deploy`. Do not edit `.cf-orbs/wrangler.jsonc`: deploy writes it again.
169
+
170
+ | Key | Default | Meaning |
171
+ | --- | --- | --- |
172
+ | `name` | `cf-orbs` | Worker name. A second install needs another name |
173
+ | `instance` | `{vcpu: 4, memoryMib: 12288, diskMb: 20000}` | Orb size: custom (1-4 vCPU, at least 3 GiB per vCPU, up to 12 GiB, 8-20 GB disk) or `standard-1`..`standard-4`. `init --vcpu N` writes `{vcpu: N, memoryMib: N*3072, diskMb: 20000}` |
174
+ | `maxRunning` | 20 | Cap on awake orbs. The next spawn fails with a clear message |
175
+ | `maxAwakeHours` | 12 | The watchdog pauses an orb that is awake longer than this |
176
+ | `checkpointMinutes` | 0 (off) | Snapshot an awake orb with changes every N minutes (15 or more) |
177
+ | `warmMinutes` | 10 | After an Amp pause, keep the orb running this many minutes (0-120). The disk is saved first. A message in that time wakes it in under 1 s. Billed like an awake orb (default size ≈ $0.11/h, ≤ $0.02 per pause). 0 = stop at once |
178
+ | `hugePages` | `true` | Each spawn sets transparent huge pages to `always` in the orb (the guest kernel default is `never`). New memory is 3-4x faster to allocate; the work bench 512 MB write went from 1.33 to 0.91 s. `false` leaves the kernel default |
179
+ | `prewarm` | 2 | After each deploy, start N throwaway instances (0-5) from the new image, wait until they answer, destroy them. This raises the odds that the next spawn finds the 1.8 GB image on its host (6 s, not 40-50 s). Cloudflare picks the host, so it is no guarantee. Cost: about 1 min of the orb size per instance. 0 = off |
180
+ | `fastTmpfsGb` | 0 (off) | Each spawn mounts a memory-backed tmpfs of N GiB (`huge=always`) at `/mnt/fast`, for caches and temp data. 512 MB writes in 0.06-0.21 s there, 1.3 s on the root disk. Not in snapshots: lost on pause. Uses memory only for the files in it. 0-8, and at most the orb memory in GiB minus 3 |
181
+ | `projectSnapshotRefresh` | `"off"` | `"on-change"`: when a new thread starts from a project snapshot, cf-orbs forgets the snapshot if amp updated itself or the orb image digest changed (at most once per project per 24 h). The next new thread runs setup. If Amp keeps its cached copy, delete it in Amp → Orb Settings → Snapshots |
182
+ | `providerHost` | `cloudflare` | Where `amp orb provider` runs: in your Worker, or `external` (your own host; see [provider/README.md](https://github.com/meirdick/cf-orbs/blob/main/provider/README.md)) |
183
+ | `images` | `{}` | Extra `ORB_IMAGE` refs → image keys. Each key needs `image/<key>.Dockerfile` |
184
+ | `debugExec` | `false` | Turns on the exec and bench routes. Only for the substrate benchmark |
185
+ | `debugSsh` | `false` | Turns on `wrangler containers ssh` for orbs |
186
+
187
+ To add toolchains to every orb, edit `image/Dockerfile` (see [image/README.md](https://github.com/meirdick/cf-orbs/blob/main/image/README.md)). Repo setup for one project belongs in that repo's `.agents/setup`.
188
+
189
+ After `cf-orbs deploy`, an orb that is awake keeps the old Worker code until it pauses (a Cloudflare behaviour). Config changes reach it at the same time.
190
+
191
+ `cf-orbs deploy` then starts `prewarm` throwaway instances in the background (`prewarm: started 2 instances in the background`); `cf-orbs status` shows how fast they were ready.
192
+
193
+ ## Troubleshooting
194
+
195
+ Run `cf-orbs doctor` first.
196
+
197
+ | You see | Do this |
198
+ | --- | --- |
199
+ | `Docker is not running` | Start Docker, check `docker info`, run the command again |
200
+ | `wrangler is not logged in` | `npx wrangler login`, run again |
201
+ | `this login has N Cloudflare accounts` | Add `--account-id <id>` or set `CLOUDFLARE_ACCOUNT_ID` |
202
+ | `Containers need the Workers Paid plan` | Upgrade the account, run the same command again |
203
+ | `this account has no workers.dev subdomain yet` | Run `init` in a terminal (it asks), or add `--subdomain <name>` |
204
+ | `registry push timed out` or `image preparation returned 401`, with `retry n of 3` | Nothing. `init` and `deploy` retry by themselves |
205
+ | `image push to registry.cloudflare.com kept failing (timeout or 401)` | Run `cf-orbs deploy` again. The registry keeps the layers that it already has |
206
+ | Amp's setup log shows `Repository configuration failed` | Amp reuses a cached project snapshot for 72 h, also a bad one. Delete it in Amp → Orb Settings → Snapshots (or `amp projects snapshots delete <owner/repo> --resource <size>`), then `cf-orbs snapshots forget --all-project`. Do the same after any change to `image/Dockerfile` or `.agents/setup` |
207
+ | `Amp rejected the provider token` | Make a new token in Amp Settings → Orbs, then `cf-orbs init --token-file <file>` |
208
+ | `another amp orb provider already uses this token` | Two providers on one token: the second one waits and takes no jobs. Stop the other one (or use a separate token), then run `init` again |
209
+ | `no Amp provider token (the Worker has none after external mode)` | Set `AMP_ORB_PROVIDER_TOKEN` or add `--token-file` |
210
+ | `provider did not report running` | `cf-orbs logs --provider`. A cron restarts the provider every 5 min |
211
+ | An orb does not start | `cf-orbs logs <orb>` shows its redacted bootstrap log; `cf-orbs status <orb>` shows its state and last spawns |
212
+ | `<n>/<max> orbs already running` | Raise `maxRunning`, or let idle threads pause |
213
+
214
+ Worker logs: `npx wrangler tail --config .cf-orbs/wrangler.jsonc`.
215
+
216
+ ## Security
217
+
218
+ What leaves your network, and what stays:
219
+
220
+ - The provider connects out to Amp (`ampcode.com`) for jobs. Nothing connects in.
221
+ - Each orb connects out to `ampcode.com` (tools) and `tunnel.ampcode.com` (portals, Terminal).
222
+ - Amp runs the agent and the model on its own servers. What the agent reads in an orb (code, command output) goes to Amp and its model providers, as with Amp-hosted orbs.
223
+ - The repo checkout, build output and orb disks stay in your Cloudflare account.
224
+
225
+ Tokens:
226
+
227
+ - **Amp provider token.** A Worker secret, passed only to the provider container. In external mode it stays on your host. Never in an orb, never logged.
228
+ - **Control token.** `.cf-orbs/control-token` (mode 600, gitignored) and the Worker secret `CONTROL_TOKEN`. It is the bearer for the public API at `https://<worker>.<subdomain>.workers.dev/v1`. Whoever has it can start, pause and delete orbs and read their redacted logs. Keep it like a password.
229
+ - **ORB_TOKEN.** Amp makes a new one for each spawn. cf-orbs passes it to `amp orb bootstrap` in memory and never writes it to disk.
230
+ - Logs and API responses go through redaction (`aop_live_…`, tokens, JWTs, long base64 strings).
231
+
232
+ Other notes:
233
+
234
+ - **Egress is open.** An orb can reach any host on the internet, the same as a laptop. There is no allowlist mode in v1.
235
+ - Keep `debugExec` off. When it is on, the control token can run any command in any orb.
236
+ - The container API cannot delete snapshots. Cloudflare keeps snapshot data for up to 30 days after the last restore, also after `cf-orbs uninstall`, unless `cf-orbs snapshots prune --yes` deletes it.
237
+ - The orb user has passwordless sudo (Amp's image needs it).
238
+
239
+ To report a vulnerability, see [SECURITY.md](https://github.com/meirdick/cf-orbs/blob/main/SECURITY.md).
240
+
241
+ ## Status and known gaps
242
+
243
+ v0.1. The control plane, dispatch program, provider host and CLI are built and unit tested. On 2026-10-30 a real Amp thread ran end to end on a fresh install: spawn from the image, setup, pause with a project snapshot, wake from the snapshot in 2.3 s, first answer 41 s after the message.
244
+
245
+ Known gaps:
246
+
247
+ - `init` has no orb self-test yet, and no Workers Paid check before the first deploy (a Free account finds out from the deploy error).
248
+ - Paused orbs restore after an image rebuild. A restored disk keeps its old content; new image content reaches only orbs started from the image.
249
+ - Orb lost mid-turn: Amp spawns the orb again and the agent runs the failed tool call again. The disk is kept; running processes and shell state are lost.
250
+ - Not tested yet: the provider's memory on `lite` over 24 h, restore after the source image is deleted from the registry, the Desktop pane, and Docker inside an orb. Snapshot storage has no published price (beta).
251
+ - Seen once: an orb's Durable Object reset during a long spawn wait. The op failed and Amp retried.
252
+ - Limits: 4 vCPU, 12 GiB memory and 20 GB disk per orb at most (Amp-hosted orbs have 60 GB). One size per install.
253
+ - Writes of new data are slower on the root disk; use `/mnt/fast` for caches (set `fastTmpfsGb`). See [docs/benchmarks.md](https://github.com/meirdick/cf-orbs/blob/main/docs/benchmarks.md#known-limits-measured).
254
+
255
+ Both platforms are young: Amp self-hosted orbs shipped on Oct 22, 2026, and Container snapshots are in beta. Expect changes.
256
+
257
+ ## Benchmarks
258
+
259
+ Method, results and costs: [docs/benchmarks.md](https://github.com/meirdick/cf-orbs/blob/main/docs/benchmarks.md). The substrate bench (cold start, restore, pause, parallel restores) runs from a clone of this repo and needs no Amp thread. Steps are in that file.
260
+
261
+ ## Docs
262
+
263
+ | Doc | Covers |
264
+ | --- | --- |
265
+ | [AGENTS.md](https://github.com/meirdick/cf-orbs/blob/main/AGENTS.md) | Install guide for an AI agent at a terminal |
266
+ | [Provider host](https://github.com/meirdick/cf-orbs/blob/main/provider/README.md) | Cloudflare-hosted vs external provider, token rotation |
267
+ | [Orb image](https://github.com/meirdick/cf-orbs/blob/main/image/README.md) | The pinned image, toolchains, why snapshots are tied to it |
268
+ | [Dispatch](https://github.com/meirdick/cf-orbs/blob/main/dispatch/README.md) | The program Amp runs per job, and its protocol |
269
+ | [Architecture](https://github.com/meirdick/cf-orbs/blob/main/docs/architecture.md) | Parts, control flow, spawn and pause, billing |
270
+ | [Benchmarks](https://github.com/meirdick/cf-orbs/blob/main/docs/benchmarks.md) | Method, results, costs, how to run the substrate bench |
271
+ | [Amp orb provider contract](https://github.com/meirdick/cf-orbs/blob/main/docs/01-amp-orb-provider-contract.md) | The Amp side: dispatch contract, images, pause, project snapshots |
272
+ | [CONTRIBUTING.md](https://github.com/meirdick/cf-orbs/blob/main/CONTRIBUTING.md) | Dev setup and tests |
273
+
274
+ ## License
275
+
276
+ Apache-2.0. See [LICENSE](https://github.com/meirdick/cf-orbs/blob/main/LICENSE) and [NOTICE](https://github.com/meirdick/cf-orbs/blob/main/NOTICE). The Amp CLI (`@ampcode/cli`) is proprietary: the provider image installs it from npm; this repo does not include it.
package/bin/cf-orbs.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../dist/index.js";
3
+
4
+ main(process.argv.slice(2)).then(
5
+ (code) => {
6
+ process.exitCode = code;
7
+ },
8
+ (e) => {
9
+ process.stderr.write(
10
+ `Failed: ${e?.message ?? e} — fix: rerun with CF_ORBS_DEBUG=1 and report the output\n`,
11
+ );
12
+ process.exitCode = 1;
13
+ },
14
+ );
package/dist/api.js ADDED
@@ -0,0 +1,124 @@
1
+ // Client for the control plane's public /v1 routes (bearer CONTROL_TOKEN).
2
+ // Routes used (the Worker must serve these when CONTROL_TOKEN is set):
3
+ // GET /v1/health → 200 {ok:true,...}
4
+ // GET /v1/status → {registry: RegistrySummary, provider: ProviderStatus}; read loosely
5
+ // GET /v1/orbs/:id → OrbStatus (worker/src/core/types.ts)
6
+ // GET /v1/orbs/:id/logs?lines=N → {containerRunning, log: string}
7
+ // POST /v1/provider/start → start ProviderHost now (init only; doctor never calls it). Optional:
8
+ // on 404 init waits for the 5-min cron instead.
9
+ // POST /v1/provider/restart?ifImageChanged=1 → restart the provider if its image changed (deploy, upgrade);
10
+ // {action, reason, restarted, ...ProviderStatus}
11
+ // GET /v1/project-snapshots → {projectSnapshots: ProjectSnapshotRow[], summary: RegistrySummary}
12
+ // DELETE /v1/project-snapshots/:id → {snapshotId, forgotten: boolean}
13
+ // GET /v1/snapshots?candidates=1&olderThan=<s> → PruneResult (worker/src/core/registry-core.ts)
14
+ // POST /v1/snapshots/forget-many {ids} → {forgotten: string[], mapped: string[]}
15
+ // POST /v1/warm → start prewarm (deploy, init); {count, started, orbIds}
16
+ // GET /v1/warm → {prewarm: config value, last: WarmRunView | null}
17
+ import { CliError } from "./errors.js";
18
+ export const ORB_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
19
+ export function makeApi(url, token, f = fetch, timeoutMs = 20_000) {
20
+ const base = url.replace(/\/+$/, "");
21
+ const call = async (method, path, body) => {
22
+ const headers = { accept: "application/json" };
23
+ if (token)
24
+ headers.authorization = `Bearer ${token}`;
25
+ if (body !== undefined)
26
+ headers["content-type"] = "application/json";
27
+ let res;
28
+ try {
29
+ res = await f(`${base}${path}`, {
30
+ method,
31
+ headers,
32
+ body: body === undefined ? undefined : JSON.stringify(body),
33
+ signal: AbortSignal.timeout(timeoutMs),
34
+ });
35
+ }
36
+ catch (e) {
37
+ throw new CliError(`cannot reach ${base} (${e.message})`, "check your network, then run cf-orbs doctor");
38
+ }
39
+ const text = await res.text();
40
+ let parsed = text;
41
+ try {
42
+ parsed = JSON.parse(text);
43
+ }
44
+ catch { }
45
+ return { status: res.status, body: parsed, text };
46
+ };
47
+ return {
48
+ url: base,
49
+ get: (p) => call("GET", p),
50
+ post: (p, b) => call("POST", p, b),
51
+ del: (p) => call("DELETE", p),
52
+ };
53
+ }
54
+ /** Throw a one-line error for a non-2xx response. */
55
+ export function expectOk(r, what) {
56
+ if (r.status >= 200 && r.status < 300)
57
+ return r.body;
58
+ if (r.status === 401) {
59
+ throw new CliError(`${what}: control token rejected (401)`, "run cf-orbs deploy to re-send the token in .cf-orbs/control-token");
60
+ }
61
+ if (r.status === 404) {
62
+ throw new CliError(`${what}: route not found (404); the public API is off or the Worker is old`, "run cf-orbs deploy");
63
+ }
64
+ const msg = typeof r.body === "object" && r.body && "error" in r.body
65
+ ? String(r.body.error)
66
+ : r.text.slice(0, 200);
67
+ // Worker errors are already "Failed: <cause> — fix: <fix>". Keep its fix; add only what failed.
68
+ const m = /^Failed: (.*?) — fix: (.*)$/s.exec(msg.trim());
69
+ if (m)
70
+ throw new CliError(`${what}: ${m[1]} (HTTP ${r.status})`, m[2]);
71
+ throw new CliError(`${what}: HTTP ${r.status} ${msg}`, "run cf-orbs doctor");
72
+ }
73
+ export function checkOrbId(id) {
74
+ if (!ORB_ID_RE.test(id))
75
+ throw new CliError(`invalid orb id ${JSON.stringify(id)}`, "pass the orb id from cf-orbs status (e.g. amp-T-…)");
76
+ return id;
77
+ }
78
+ export function providerInfo(status) {
79
+ if (!status || typeof status !== "object")
80
+ return { logTail: [] };
81
+ const p = status.provider;
82
+ if (!p || typeof p !== "object")
83
+ return { logTail: [] };
84
+ const o = p;
85
+ const arr = [o.logTail, o.log, o.lines].find(Array.isArray);
86
+ const logTail = (arr ?? []).map(String);
87
+ const str = (v) => (typeof v === "string" ? v : undefined);
88
+ return {
89
+ running: typeof o.running === "boolean" ? o.running : undefined,
90
+ lastLogLine: str(o.lastLogLine) ?? logTail[logTail.length - 1],
91
+ logTail,
92
+ startedAt: str(o.startedAt),
93
+ restarts: typeof o.restarts === "number" ? o.restarts : undefined,
94
+ lastError: str(o.lastError) ?? str(o.error),
95
+ stopped: typeof o.stopped === "boolean" ? o.stopped : undefined,
96
+ image: str(o.image),
97
+ build: str(o.build),
98
+ deployedImage: str(o.deployedImage),
99
+ deployedBuild: str(o.deployedBuild),
100
+ };
101
+ }
102
+ /**
103
+ * Every distinct image key named anywhere in a JSON value: "imageKey" string fields (handles, mappings) and
104
+ * "imageKeys" string arrays (a Registry summary of keys that live snapshots use).
105
+ */
106
+ export function collectImageKeys(v, out = new Set()) {
107
+ if (Array.isArray(v))
108
+ for (const x of v)
109
+ collectImageKeys(x, out);
110
+ else if (v && typeof v === "object") {
111
+ for (const [k, x] of Object.entries(v)) {
112
+ if (k === "imageKey" && typeof x === "string")
113
+ out.add(x);
114
+ else if (k === "imageKeys" && Array.isArray(x)) {
115
+ for (const y of x)
116
+ if (typeof y === "string")
117
+ out.add(y);
118
+ }
119
+ else
120
+ collectImageKeys(x, out);
121
+ }
122
+ }
123
+ return out;
124
+ }