cf-orbs 0.2.1 → 0.2.2
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.
- package/README.md +15 -12
- package/dist/commands/init.js +14 -1
- package/dist/config.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/cf-orbs.config.example.jsonc +15 -1
- package/templates/dispatch/dispatch.sh +3 -3
- package/templates/image/Dockerfile +2 -2
- package/templates/provider/Dockerfile +2 -2
- package/templates/provider/entry.sh +1 -1
- package/templates/worker/src/api.ts +1 -1
- package/templates/worker/src/core/config.ts +3 -2
- package/templates/worker/src/core/types.ts +7 -3
package/README.md
CHANGED
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
+
Docs: [cf-orbs-site.mdick85.workers.dev/docs](https://cf-orbs-site.mdick85.workers.dev/docs/)
|
|
6
|
+
|
|
5
7
|
Community project. Not affiliated with Cloudflare or Amp (Sourcegraph). "Cloudflare" and "Amp" are trademarks of their owners.
|
|
6
8
|
|
|
7
9
|
## Why cf-orbs
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Amp's self-hosted orbs work like this: you provide the compute, and Amp turns it into orbs. cf-orbs makes your Cloudflare account that compute. You keep the same Amp threads and the same orb experience, on machines you own.
|
|
12
|
+
|
|
13
|
+
- **Your account.** Orbs, snapshots and logs stay in your Cloudflare account. The provider connects out to Amp. Nothing connects in.
|
|
14
|
+
- **No server to run.** One CLI, no cluster, no extra API keys. Cloudflare runs the machines.
|
|
15
|
+
- **Your image.** Orbs start from your own image and toolchain (`image/Dockerfile`).
|
|
16
|
+
- **Your bill.** You pay Cloudflare. A paused orb has no compute charge.
|
|
17
|
+
- **New threads start ready.** Repo setup runs once. Each new thread then starts from that project snapshot, with your dependencies already installed.
|
|
18
|
+
- **Recovers by itself.** A lost orb comes back and the agent runs the failed tool call again. Paused orbs restore after an image rebuild.
|
|
10
19
|
|
|
11
|
-
|
|
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.
|
|
20
|
+
Benchmarked as fast or faster than Amp-hosted orbs on first response and installs ([benchmarks](https://cf-orbs-site.mdick85.workers.dev/benchmarks), [method](https://github.com/meirdick/cf-orbs/blob/main/docs/benchmarks.md#method)).
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
Good to know: a paused orb keeps its files, not running processes (Cloudflare saves the disk only). One orb is at most 4 vCPU, 12 GiB memory and 20 GB disk.
|
|
20
23
|
|
|
21
24
|
## What it is
|
|
22
25
|
|
|
@@ -49,7 +52,7 @@ mkdir my-orbs && cd my-orbs
|
|
|
49
52
|
npx cf-orbs init --provider-name <name from Amp>
|
|
50
53
|
```
|
|
51
54
|
|
|
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.
|
|
55
|
+
`init` asks for the Amp provider token. The prompt names the Amp page where you create it. It does not show what you type, and the token does not go into your shell history.
|
|
53
56
|
|
|
54
57
|
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
58
|
|
|
@@ -202,7 +205,7 @@ Every command takes `--json`, `--dir <project>` and `--non-interactive`. Orb ids
|
|
|
202
205
|
| Key | Default | Meaning |
|
|
203
206
|
| --- | --- | --- |
|
|
204
207
|
| `name` | `cf-orbs` | Worker name. A second install needs another name |
|
|
205
|
-
| `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}` |
|
|
208
|
+
| `instance` | `{vcpu: 4, memoryMib: 12288, diskMb: 20000}` | Orb size: custom (1-4 vCPU, fractions allowed, 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}` |
|
|
206
209
|
| `maxRunning` | 20 | Cap on awake orbs. The next spawn fails with a clear message |
|
|
207
210
|
| `maxAwakeHours` | 12 | The watchdog pauses an orb that is awake longer than this |
|
|
208
211
|
| `checkpointMinutes` | 0 (off) | Snapshot an awake orb with changes every N minutes (15 or more) |
|
|
@@ -272,7 +275,7 @@ To report a vulnerability, see [SECURITY.md](https://github.com/meirdick/cf-orbs
|
|
|
272
275
|
|
|
273
276
|
## Status and known gaps
|
|
274
277
|
|
|
275
|
-
v0.2.
|
|
278
|
+
v0.2.2. 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.
|
|
276
279
|
|
|
277
280
|
Known gaps:
|
|
278
281
|
|
package/dist/commands/init.js
CHANGED
|
@@ -339,6 +339,10 @@ export async function runInit(ctx, o, steps) {
|
|
|
339
339
|
ui.step("control token: reusing .cf-orbs/control-token");
|
|
340
340
|
break;
|
|
341
341
|
case "amp-token":
|
|
342
|
+
// No interactive name question here: say so if the name is a fallback, not given
|
|
343
|
+
if (s.source !== "prompt" &&
|
|
344
|
+
(o.providerNameSource === "config" || o.providerNameSource === "default"))
|
|
345
|
+
ui.warn(`provider name ${providerName} was not given explicitly; it must match the name in Amp Settings → Orbs (pass --provider-name <name> if it differs)`);
|
|
342
346
|
if (s.source === "env")
|
|
343
347
|
ampToken = checkAmpToken(ctx, (ctx.env.AMP_ORB_PROVIDER_TOKEN ?? "").trim());
|
|
344
348
|
else if (s.source === "file") {
|
|
@@ -355,7 +359,7 @@ export async function runInit(ctx, o, steps) {
|
|
|
355
359
|
: keep
|
|
356
360
|
? "; Enter keeps the current one"
|
|
357
361
|
: "";
|
|
358
|
-
const t = await askHidden(`Amp provider token (aop_live_…, not shown)${enter}: `);
|
|
362
|
+
const t = await askHidden(`Amp provider token (aop_live_…, not shown; create or copy it in Amp Settings → Orbs → Add Provider)${enter}: `);
|
|
359
363
|
if (t === "" && o.noDeploy)
|
|
360
364
|
ui.step("amp token: not given; set it later (see the steps below)");
|
|
361
365
|
else if (t === "" && keep)
|
|
@@ -766,6 +770,15 @@ export function initOptions(ctx, existing) {
|
|
|
766
770
|
existing?.providerName ??
|
|
767
771
|
existing?.name ??
|
|
768
772
|
"cf-orbs",
|
|
773
|
+
providerNameSource: flagString(ctx.args, "provider-name") !== undefined
|
|
774
|
+
? "flag"
|
|
775
|
+
: ctx.env.AMP_ORB_PROVIDER_NAME !== undefined
|
|
776
|
+
? "env"
|
|
777
|
+
: existing?.providerName !== undefined
|
|
778
|
+
? "saved"
|
|
779
|
+
: existing?.name !== undefined
|
|
780
|
+
? "config"
|
|
781
|
+
: "default",
|
|
769
782
|
};
|
|
770
783
|
}
|
|
771
784
|
export async function initCommand(ctx) {
|
package/dist/config.js
CHANGED
|
@@ -307,7 +307,7 @@ export function validateInstance(v) {
|
|
|
307
307
|
export function instanceMemoryMib(i) {
|
|
308
308
|
if (typeof i === "object")
|
|
309
309
|
return i.memoryMib;
|
|
310
|
-
return {
|
|
310
|
+
return { "standard-1": 4096, "standard-2": 6144, "standard-3": 8192, "standard-4": 12288 }[i];
|
|
311
311
|
}
|
|
312
312
|
/** fastTmpfsGb: 0-8, and at most the orb memory minus 3 GiB. Same rule as worker validateFastTmpfs. */
|
|
313
313
|
export function validateFastTmpfs(v, instance) {
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export const COMMANDS = {
|
|
|
30
30
|
"--provider cloudflare|external where amp orb provider runs (default cloudflare)",
|
|
31
31
|
"--token-file <file> Amp provider token file (else AMP_ORB_PROVIDER_TOKEN, else prompt)",
|
|
32
32
|
"--account-id <id> Cloudflare account (else CLOUDFLARE_ACCOUNT_ID, else ask)",
|
|
33
|
-
"--provider-name <name> Amp provider name, as in Amp Settings → Orbs (
|
|
33
|
+
"--provider-name <name> Amp provider name, as in Amp Settings → Orbs (else AMP_ORB_PROVIDER_NAME, saved name, config name, then cf-orbs)",
|
|
34
34
|
"--subdomain <name> workers.dev subdomain to register if the account has none",
|
|
35
35
|
"--vcpu <1|2|3|4> orb size: N vCPU, N x 3 GiB, 20 GB disk (default 4; 2 is cheaper)",
|
|
36
36
|
"--name <name> Worker name (default cf-orbs; a second install needs another)",
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "cf-orbs",
|
|
6
6
|
|
|
7
7
|
// Orb size. One size per install: Amp sends no size to the provider.
|
|
8
|
-
// Custom: vcpu 1-4, memoryMib up to 12288 and at least 3072 per vCPU, diskMb 8000-20000.
|
|
8
|
+
// Custom: vcpu 1-4 (fractions allowed), memoryMib up to 12288 and at least 3072 per vCPU, diskMb 8000-20000.
|
|
9
9
|
// Or a named type: "standard-1" .. "standard-4". "lite" and "basic" are too small for the orb image.
|
|
10
10
|
// Default 4 vCPU, 12 GiB, 20 GB. `cf-orbs init --vcpu N` writes N vCPU, N x 3 GiB, 20 GB (--vcpu 2 is cheaper).
|
|
11
11
|
"instance": { "vcpu": 4, "memoryMib": 12288, "diskMb": 20000 },
|
|
@@ -53,4 +53,18 @@
|
|
|
53
53
|
|
|
54
54
|
// Enable wrangler containers ssh for orbs (debug only).
|
|
55
55
|
"debugSsh": false
|
|
56
|
+
|
|
57
|
+
// Turn on the exec and bench routes. Only for the substrate benchmark: the control token can then run
|
|
58
|
+
// commands in orbs. true or false.
|
|
59
|
+
// "debugExec": false,
|
|
60
|
+
|
|
61
|
+
// Minutes an orb may sit idle before the container stops. 5-360.
|
|
62
|
+
// "inactivityTimeoutMinutes": 15,
|
|
63
|
+
|
|
64
|
+
// Seconds between watchdog checks of an orb. 10-3600, and shorter than inactivityTimeoutMinutes.
|
|
65
|
+
// "alarmSeconds": 60,
|
|
66
|
+
|
|
67
|
+
// Digest of the Amp orb image the "orb" image is built from. Used for log lines only.
|
|
68
|
+
// Must look like sha256:<64 hex digits>.
|
|
69
|
+
// "ampImageDigest": "sha256:378c5e2188f00960eca36dc806aa04e1cacd92c2341665bcdf4692326c462d6d"
|
|
56
70
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# cf-orbs dispatch program. `amp orb provider` runs it once per job
|
|
2
|
+
# cf-orbs dispatch program. `amp orb provider` runs it once per job.
|
|
3
3
|
#
|
|
4
4
|
# Input (env, from Amp): ORB_OPERATION ORB_ID ORB_IMAGE ORB_SNAPSHOT_ID ORB_TOKEN AMP_URL
|
|
5
5
|
# Input (env, from cf-orbs):
|
|
@@ -190,7 +190,7 @@ redact() { CFO_CTL=$CTL_TOKEN awk "$REDACT_AWK"; }
|
|
|
190
190
|
say() { printf '%s\n' "$*" | redact; }
|
|
191
191
|
|
|
192
192
|
# fail <message> [exit code]. One final line on stdout and stderr. Worker errors already start
|
|
193
|
-
# with "Failed: "; drop it so the line has one prefix
|
|
193
|
+
# with "Failed: "; drop it so the line has one prefix.
|
|
194
194
|
fail() {
|
|
195
195
|
msg=$1
|
|
196
196
|
while :; do
|
|
@@ -223,7 +223,7 @@ case $OP in
|
|
|
223
223
|
*) fail "unknown ORB_OPERATION '$(clean "$OP")' — fix: update cf-orbs (npx cf-orbs@latest deploy)" 2 ;;
|
|
224
224
|
esac
|
|
225
225
|
|
|
226
|
-
# Same rule as the server
|
|
226
|
+
# Same rule as the server: ^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$
|
|
227
227
|
orb_ok=1
|
|
228
228
|
case $ORB in
|
|
229
229
|
"" | [!A-Za-z0-9]* | *[!A-Za-z0-9_-]*) orb_ok=0 ;;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# cf-orbs orb image
|
|
1
|
+
# cf-orbs orb image. Built by `cf-orbs deploy` (wrangler) and stored under the image key "orb".
|
|
2
2
|
#
|
|
3
3
|
# Why a digest pin and not :latest
|
|
4
4
|
# A snapshot only restores onto the image it was taken from. A moving tag gives a new image on the
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# The base image has no init and its CMD (python3) exits at once. tini is PID 1, keeps the container
|
|
11
11
|
# up with `sleep infinity`, and reaps the processes that cf-orbs starts with setsid (amp orb bootstrap
|
|
12
12
|
# and its children). -s makes tini a subreaper, so orphans of those processes are reaped too.
|
|
13
|
-
# tini is pinned to one Debian version so that a rebuild without cache gives the same layer
|
|
13
|
+
# tini is pinned to one Debian version so that a rebuild without cache gives the same layer.
|
|
14
14
|
# If apt cannot find that version any more, bump it and treat the result as a new image.
|
|
15
15
|
#
|
|
16
16
|
# How to add toolchains
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# cf-orbs provider image
|
|
1
|
+
# cf-orbs provider image. Runs `amp orb provider /app/dispatch.sh` under tini.
|
|
2
2
|
# Build context is the repo root (wrangler image entry: build_context ".."):
|
|
3
3
|
# docker build -f provider/Dockerfile -t cf-orbs-provider .
|
|
4
4
|
FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c
|
|
@@ -6,7 +6,7 @@ FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc
|
|
|
6
6
|
# Pinned Amp CLI. Bump with Renovate or by hand; the provider must match Amp's job protocol.
|
|
7
7
|
ARG AMP_CLI_VERSION=0.0.1793390435-g08e9ec
|
|
8
8
|
|
|
9
|
-
# curl: dispatch.sh
|
|
9
|
+
# curl: dispatch.sh. tini: PID 1 that forwards signals and reaps children.
|
|
10
10
|
RUN apt-get update \
|
|
11
11
|
&& apt-get install -y --no-install-recommends curl ca-certificates tini \
|
|
12
12
|
&& rm -rf /var/lib/apt/lists/*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# cf-orbs provider entry
|
|
2
|
+
# cf-orbs provider entry. Runs `amp orb provider <dispatch>` in a loop.
|
|
3
3
|
# Output is redacted, then written to stdout and to the log file. The log is capped.
|
|
4
4
|
# Env:
|
|
5
5
|
# AMP_ORB_PROVIDER_TOKEN required. Read by amp; never printed.
|
|
@@ -18,7 +18,7 @@ import { type Env, orbDoName, PROVIDER_DO_NAME, REGISTRY_DO_NAME } from "./env";
|
|
|
18
18
|
import { BENCH_ORB_RE, type BenchStartRequest, parseBenchStart } from "./orb/bench";
|
|
19
19
|
import { restartProvider } from "./provider-restart";
|
|
20
20
|
|
|
21
|
-
export const API_VERSION = "0.
|
|
21
|
+
export const API_VERSION = "0.2.2";
|
|
22
22
|
|
|
23
23
|
/** D2. Amp sends `amp-<thread id>`. Never starts with `_` or `.`, so it cannot look like an internal name. */
|
|
24
24
|
export const ORB_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
DEFAULT_CONFIG,
|
|
6
6
|
type InstanceSpec,
|
|
7
7
|
MAX_PREWARM,
|
|
8
|
+
type OrbInstanceSpec,
|
|
8
9
|
type ProjectSnapshotRefresh,
|
|
9
10
|
type ProviderConfig,
|
|
10
11
|
} from "./types";
|
|
@@ -37,9 +38,9 @@ function num(v: unknown, name: string, min: number, max: number, integer = true)
|
|
|
37
38
|
return v;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export function validateInstance(v: unknown):
|
|
41
|
+
export function validateInstance(v: unknown): OrbInstanceSpec {
|
|
41
42
|
if (typeof v === "string") {
|
|
42
|
-
if ((NAMED as readonly string[]).includes(v)) return v as
|
|
43
|
+
if ((NAMED as readonly string[]).includes(v)) return v as OrbInstanceSpec;
|
|
43
44
|
if (v === "lite" || v === "basic" || v === "dev") {
|
|
44
45
|
fail(
|
|
45
46
|
`instance ${v} is too small for the orb image`,
|
|
@@ -49,7 +49,7 @@ export interface SnapshotHandle {
|
|
|
49
49
|
id: string; // Cloudflare snapshot id
|
|
50
50
|
size: number; // bytes reported by snapshotContainer
|
|
51
51
|
imageKey: string; // key in images map the snapshot was taken from
|
|
52
|
-
instance:
|
|
52
|
+
instance: OrbInstanceSpec;
|
|
53
53
|
createdAt: string; // ISO
|
|
54
54
|
lastRestoredAt?: string; // ISO; set on every successful restore. Expiry = max(createdAt, lastRestoredAt) + 30 d (D7)
|
|
55
55
|
name?: string;
|
|
@@ -70,6 +70,7 @@ export interface SpawnEntry {
|
|
|
70
70
|
/** OrbRecord.spawns keeps this many entries, oldest first. */
|
|
71
71
|
export const SPAWN_HISTORY = 10;
|
|
72
72
|
|
|
73
|
+
/** Any Cloudflare Containers instance size. "lite" (256 MiB) is used only by the provider container. */
|
|
73
74
|
export type InstanceSpec =
|
|
74
75
|
| "lite"
|
|
75
76
|
| "standard-1"
|
|
@@ -78,6 +79,9 @@ export type InstanceSpec =
|
|
|
78
79
|
| "standard-4"
|
|
79
80
|
| { vcpu: number; memoryMib: number; diskMb: number };
|
|
80
81
|
|
|
82
|
+
/** An orb size. "lite" is too small for the orb image, so config validation rejects it. */
|
|
83
|
+
export type OrbInstanceSpec = Exclude<InstanceSpec, "lite">;
|
|
84
|
+
|
|
81
85
|
/** Per-orb record in OrbSession storage under key "orb". */
|
|
82
86
|
export interface OrbRecord {
|
|
83
87
|
/** Schema version (D13). Old and new Worker code share storage; a missing v reads as 1. */
|
|
@@ -85,7 +89,7 @@ export interface OrbRecord {
|
|
|
85
89
|
orbId: string;
|
|
86
90
|
state: OrbState;
|
|
87
91
|
imageKey: string;
|
|
88
|
-
instance:
|
|
92
|
+
instance: OrbInstanceSpec;
|
|
89
93
|
handle?: SnapshotHandle; // per-orb snapshot (last pause or checkpoint)
|
|
90
94
|
bootstrapTokenSha?: string; // first 16 hex of sha256(ORB_TOKEN) for the running bootstrap
|
|
91
95
|
awakeSince?: string;
|
|
@@ -136,7 +140,7 @@ export type ProjectSnapshotRefresh = "off" | "on-change";
|
|
|
136
140
|
|
|
137
141
|
/** Provider-level configuration, derived from cf-orbs.config.jsonc and passed as Worker vars (JSON string in CF_ORBS_CONFIG). */
|
|
138
142
|
export interface ProviderConfig {
|
|
139
|
-
instance:
|
|
143
|
+
instance: OrbInstanceSpec; // default { vcpu: 4, memoryMib: 12288, diskMb: 20000 }
|
|
140
144
|
maxRunning: number; // default 20
|
|
141
145
|
maxAwakeHours: number; // default 12
|
|
142
146
|
checkpointMinutes: number; // default 0 = off
|