@desplega.ai/agent-env 0.0.0-stage → 0.3.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.
- package/LICENSE +21 -0
- package/README.md +343 -2
- package/assets/icon.svg +5 -0
- package/bin/agent-env.mjs +210 -0
- package/docs/agent-swarm.md +146 -0
- package/docs/releases.md +207 -0
- package/docs/screenshots/dashboard-desktop.png +0 -0
- package/docs/screenshots/dashboard-mobile.png +0 -0
- package/docs/snapshots-and-worktrees.md +221 -0
- package/examples/agent-swarm/README.md +5 -0
- package/examples/agent-swarm/agent-env.json +11 -0
- package/examples/agent-swarm/compose.agent-env.yaml +101 -0
- package/examples/agent-swarm/verify.mjs +55 -0
- package/examples/compose/.dockerignore +3 -0
- package/examples/compose/Dockerfile +4 -0
- package/examples/compose/README.md +109 -0
- package/examples/compose/compose.yaml +36 -0
- package/examples/compose/server.mjs +6 -0
- package/examples/hello/agent-env.json +20 -0
- package/examples/hello/index.html +12 -0
- package/package.json +49 -4
- package/src/cloud.mjs +314 -0
- package/src/config.mjs +133 -0
- package/src/daemon.mjs +484 -0
- package/src/environments.mjs +207 -0
- package/src/goodies.mjs +71 -0
- package/src/init.mjs +68 -0
- package/src/locks.mjs +56 -0
- package/src/maintenance.mjs +74 -0
- package/src/paths.mjs +5 -0
- package/src/providers/CLOUD_SANDBOX.md +240 -0
- package/src/providers/README.md +62 -0
- package/src/providers/compose.mjs +460 -0
- package/src/providers/docker.mjs +207 -0
- package/src/providers/index.mjs +13 -0
- package/src/providers/sandbox.mjs +170 -0
- package/src/proxy.mjs +149 -0
- package/src/runtime.mjs +83 -0
- package/src/snapshots.mjs +394 -0
- package/src/source-paths.mjs +15 -0
- package/src/system.mjs +39 -0
- package/src/tls.mjs +57 -0
- package/src/ui-client.js +51 -0
- package/src/ui.html +12 -0
- package/src/ui.mjs +32 -0
- package/src/watch.mjs +20 -0
- package/src/worktrees.mjs +154 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Owned cloud hosts and sandbox adapters
|
|
2
|
+
|
|
3
|
+
The provider registry includes `SandboxProvider` under `sandbox`.
|
|
4
|
+
The daemon owns scheduling, instance state, and service startup.
|
|
5
|
+
|
|
6
|
+
## Cloud integration
|
|
7
|
+
|
|
8
|
+
Import these functions from `src/cloud.mjs`:
|
|
9
|
+
|
|
10
|
+
| Export | Contract |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `prepareCloud(spec, existingHandle?)` | Create or resume one owned Hetzner host. Return a new spec with `host`, `sshConfig`, and stable `cloud` metadata. |
|
|
13
|
+
| `commitCloud(handle)` | Mark infrastructure as retained after the first successful provider start. Return updated cloud metadata. |
|
|
14
|
+
| `sleepCloud(handle)` | Request graceful shutdown and wait for the server to stop. Preserve the server disk, address, and key. |
|
|
15
|
+
| `destroyCloud(handle)` | Purge the owned server, primary IPv4 address, remote SSH key, and local cloud record. |
|
|
16
|
+
| `cloudStatus(handle)` | Return cloud metadata, server `status`, `committed`, and `destroying`. Never create infrastructure. |
|
|
17
|
+
| `disposeFailedCloud(handle)` | Purge an uncommitted first start. Return `{destroyed: false, reason: 'committed'}` for retained infrastructure. |
|
|
18
|
+
| `cloudOptions(options)` | Validate cloud options and apply defaults. |
|
|
19
|
+
| `cloudInit(privateHostKey, publicHostKey)` | Generate cloud-init for supported Ubuntu and Debian images. The result contains a private key. Never log it. |
|
|
20
|
+
| `createCloudManager(options)` | Create an isolated manager with optional `home`, `run`, `delay`, `now`, and `readinessTimeoutMs` dependencies. |
|
|
21
|
+
|
|
22
|
+
Every lifecycle function accepts returned cloud metadata, a prepared spec, or a provider handle containing `spec.cloud`.
|
|
23
|
+
`prepareCloud` also accepts a persisted handle as its second argument.
|
|
24
|
+
The spec must contain safe `owner` and `name` values.
|
|
25
|
+
The cloud module does not require `provider: 'docker'`.
|
|
26
|
+
It only supplies the SSH transport fields that the existing Docker provider understands.
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const spec = {
|
|
30
|
+
owner: installationOwner,
|
|
31
|
+
name: 'demo',
|
|
32
|
+
provider: 'docker',
|
|
33
|
+
cwd: '/absolute/checkout',
|
|
34
|
+
services,
|
|
35
|
+
cloud: {
|
|
36
|
+
provider: 'hetzner',
|
|
37
|
+
type: 'cx23',
|
|
38
|
+
location: 'fsn1',
|
|
39
|
+
image: 'ubuntu-24.04',
|
|
40
|
+
idleTimeoutMs: 1800000,
|
|
41
|
+
expiresAt: null,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let prepared;
|
|
46
|
+
let runtime;
|
|
47
|
+
try {
|
|
48
|
+
prepared = await prepareCloud(spec, persistedHandle);
|
|
49
|
+
runtime = persistedHandle
|
|
50
|
+
? { ...persistedHandle, spec: prepared }
|
|
51
|
+
: await provider.create(prepared);
|
|
52
|
+
if (persistedHandle) await provider.start(runtime);
|
|
53
|
+
// Persist the runtime before commit. Retain it if a later commit attempt fails.
|
|
54
|
+
await persistInstance(runtime);
|
|
55
|
+
runtime.spec.cloud = await commitCloud(prepared);
|
|
56
|
+
await persistInstance(runtime);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Call this only when no successful runtime needs retention.
|
|
59
|
+
// A failed commit or persistence call needs reconciliation, not automatic purge.
|
|
60
|
+
if (!runtime) {
|
|
61
|
+
const cleanup = prepared ?? error.cloud;
|
|
62
|
+
if (cleanup) await disposeFailedCloud(cleanup);
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The default idle timeout is 30 minutes.
|
|
69
|
+
Set `idleTimeoutMs: 0` to disable idle shutdown.
|
|
70
|
+
Set `expiresAt` to an ISO date string for explicit expiry.
|
|
71
|
+
These fields are metadata for the parent daemon.
|
|
72
|
+
The module creates no timer, cron job, or background worker.
|
|
73
|
+
The parent decides when to call `sleepCloud` or `destroyCloud`.
|
|
74
|
+
`prepareCloud` refuses an elapsed expiry until the parent extends it or destroys the instance.
|
|
75
|
+
Changing machine type, image, or location requires purge first.
|
|
76
|
+
Changing idle timeout or expiry does not replace the machine.
|
|
77
|
+
|
|
78
|
+
`cloud` metadata includes `recordId`, `owner`, `name`, `serverId`, `keyId`, `primaryIpIds`, and `createdAt`.
|
|
79
|
+
It also includes the cloud options and `createdForStart`.
|
|
80
|
+
`createdForStart` remains true until `commitCloud` succeeds.
|
|
81
|
+
Always persist `spec.cloud` with the provider handle.
|
|
82
|
+
On provisioning failure, the thrown error contains `error.cloud` when a record exists.
|
|
83
|
+
Use that metadata for cleanup or retry.
|
|
84
|
+
|
|
85
|
+
### Ownership and recovery
|
|
86
|
+
|
|
87
|
+
The module uses the installed authenticated `hcloud` CLI.
|
|
88
|
+
It creates one dedicated SSH key and one labeled primary IPv4 address before it creates the server.
|
|
89
|
+
The server uses that address and disables automatic IPv6 allocation.
|
|
90
|
+
The module creates no volumes, snapshots, or hosted credentials.
|
|
91
|
+
|
|
92
|
+
Records reside under `$AGENT_ENV_HOME/cloud/<instance-hash>/`.
|
|
93
|
+
The default home is `~/.local/state/agent-env`.
|
|
94
|
+
Directories use mode `0700`.
|
|
95
|
+
Records, private keys, SSH config, and known hosts use mode `0600`.
|
|
96
|
+
Public keys remain inside the private directory.
|
|
97
|
+
Cloud home paths cannot contain newlines, null bytes, or SSH percent tokens.
|
|
98
|
+
Records contain the context, endpoint, and a credential fingerprint. They never contain the API token.
|
|
99
|
+
Every API operation checks this binding and passes the checked credential directly through the child environment.
|
|
100
|
+
A context, endpoint, or credential change stops the operation before resource access.
|
|
101
|
+
Older records without a binding require manual recovery. The module preserves their SSH keys and ownership data.
|
|
102
|
+
Locks publish complete owner files atomically. Recovery supports dead owners and stale empty locks from older builds.
|
|
103
|
+
|
|
104
|
+
Each remote resource has these labels:
|
|
105
|
+
|
|
106
|
+
- `agent-env.owner`: the installation owner.
|
|
107
|
+
- `agent-env.instance`: the instance name.
|
|
108
|
+
- `agent-env.record`: a random ownership token from the private record.
|
|
109
|
+
|
|
110
|
+
The module stores creation intent before each external allocation.
|
|
111
|
+
It reconciles remote resources by token, name, labels, and stored ID after restart.
|
|
112
|
+
It refuses multiple matches, changed ownership, or conflicting IDs.
|
|
113
|
+
A missing recorded resource never causes automatic replacement.
|
|
114
|
+
An ambiguous creation with no visible resource also blocks replacement.
|
|
115
|
+
Use `agent-env rm NAME --purge` before a new allocation.
|
|
116
|
+
|
|
117
|
+
Purge checks every resource before its first deletion.
|
|
118
|
+
It refuses a foreign IP assignment or an untracked primary IPv6 address.
|
|
119
|
+
It retains records and private credentials after partial cleanup failure.
|
|
120
|
+
Repeat `destroyCloud` with the same handle to finish cleanup.
|
|
121
|
+
The module deletes the local record only after every owned remote resource is absent.
|
|
122
|
+
A completed purge removes the record, so another purge call rejects the missing record.
|
|
123
|
+
|
|
124
|
+
A lock file serializes operations for each instance.
|
|
125
|
+
The module recovers a lock when its recorded process no longer exists.
|
|
126
|
+
An empty, malformed, or abandoned recovery lock requires operator inspection before removal.
|
|
127
|
+
Never remove a live process lock.
|
|
128
|
+
|
|
129
|
+
Keep the same Hetzner project and authenticated CLI context during the instance lifetime.
|
|
130
|
+
Keep the ownership directory until purge completes.
|
|
131
|
+
Without that directory, the module refuses handles instead of claiming remote resources.
|
|
132
|
+
Manual record deletion can therefore leave billable infrastructure that needs operator cleanup.
|
|
133
|
+
|
|
134
|
+
### Bootstrap and SSH identity
|
|
135
|
+
|
|
136
|
+
Cloud-init installs Docker Engine, the current Compose plugin, Buildx, and rsync from Docker's official package repository.
|
|
137
|
+
The bootstrap supports Ubuntu and Debian images with cloud-init and systemd.
|
|
138
|
+
A custom image must meet those requirements.
|
|
139
|
+
Type, location, and image availability depend on the authenticated Hetzner project.
|
|
140
|
+
Live provisioning passed with CX23 in hel1. Availability remains specific to the account and location.
|
|
141
|
+
|
|
142
|
+
The module generates a dedicated host key before provisioning.
|
|
143
|
+
Cloud-init installs that key, and a private `known_hosts` file pins its public key.
|
|
144
|
+
SSH requires strict host checking and disables agent and password authentication.
|
|
145
|
+
The module never uses `ssh-keyscan` or automatic trust on first connection.
|
|
146
|
+
Readiness requires successful SSH authentication, completed cloud-init, Docker, Compose, and rsync.
|
|
147
|
+
A readiness failure preserves a cleanup handle.
|
|
148
|
+
Errors do not include captured CLI output, private keys, or credential values.
|
|
149
|
+
|
|
150
|
+
Official references:
|
|
151
|
+
|
|
152
|
+
- [Hetzner CLI server creation](https://github.com/hetznercloud/cli/blob/main/internal/cmd/server/create.go)
|
|
153
|
+
- [Hetzner CLI primary IP creation](https://github.com/hetznercloud/cli/blob/main/internal/cmd/primaryip/create.go)
|
|
154
|
+
- [Hetzner CLI SSH key creation](https://github.com/hetznercloud/cli/blob/main/internal/cmd/sshkey/create.go)
|
|
155
|
+
- [Docker packages for Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
|
|
156
|
+
|
|
157
|
+
## Sandbox integration
|
|
158
|
+
|
|
159
|
+
Import `SandboxProvider` from `src/providers/sandbox.mjs`.
|
|
160
|
+
The provider receives this configuration:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
{
|
|
164
|
+
provider: 'sandbox',
|
|
165
|
+
// Include the normal owner, name, cwd, and services fields.
|
|
166
|
+
sandbox: {
|
|
167
|
+
adapter: 'docker', // Or './my-adapter.mjs', an absolute path, or a local file URL.
|
|
168
|
+
options: {},
|
|
169
|
+
requireCapabilities: ['exec', 'stop'],
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Relative adapter paths resolve against the absolute `spec.cwd`.
|
|
175
|
+
Remote URLs and package imports are unsupported.
|
|
176
|
+
Local adapter modules execute with the daemon's privileges.
|
|
177
|
+
Use only trusted adapter code.
|
|
178
|
+
Adapters must export `createAdapter(options)` and return an object implementing the existing provider contract.
|
|
179
|
+
The factory can be asynchronous.
|
|
180
|
+
Each provider caches adapter objects by module location and options.
|
|
181
|
+
A persisted handle contains `spec`, public `services`, the adapter location, and its JSON runtime handle.
|
|
182
|
+
Runtime connections remain outside that handle.
|
|
183
|
+
|
|
184
|
+
Every adapter must declare a boolean for every exported `SANDBOX_CAPABILITIES` entry:
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
export function createAdapter(options) {
|
|
188
|
+
return {
|
|
189
|
+
capabilities: {
|
|
190
|
+
create: true, start: true, wait: true, connect: true,
|
|
191
|
+
stop: true, remove: true, status: true, sync: true,
|
|
192
|
+
logs: true, exec: true, fork: false,
|
|
193
|
+
compose: false,
|
|
194
|
+
},
|
|
195
|
+
// Implement each operation whose capability is true.
|
|
196
|
+
// Use the signatures in src/providers/README.md.
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`create`, `wait`, `connect`, and `status` must be supported.
|
|
202
|
+
Other operations can be false and omit their methods.
|
|
203
|
+
The wrapper rejects unsupported operations before it calls the adapter.
|
|
204
|
+
It also checks `sandbox.requireCapabilities` and rejects Compose specs when `compose` is false.
|
|
205
|
+
`compose` describes configuration support beyond the base lifecycle methods.
|
|
206
|
+
Sandbox snapshots have no provider contract yet, so adapters cannot request or advertise that capability.
|
|
207
|
+
The adapter must clean partial allocation when its `create` method rejects.
|
|
208
|
+
The wrapper attempts purge if an adapter returns an invalid handle and supports `remove`.
|
|
209
|
+
|
|
210
|
+
The built-in `docker` adapter delegates to the existing Docker provider.
|
|
211
|
+
It supports the existing local and SSH lifecycle, named volumes, synchronization, logs, exec, and fork behavior.
|
|
212
|
+
It declares `compose: false`. The daemon snapshot commands require the Docker or Compose provider.
|
|
213
|
+
It provides container isolation at the Docker host's trust boundary.
|
|
214
|
+
It implements neither Firecracker nor a hosted sandbox vendor.
|
|
215
|
+
It needs no hosted credentials.
|
|
216
|
+
|
|
217
|
+
A local bridge module can also reuse it:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
import { createDockerSandboxAdapter } from '/absolute/agent-env/src/providers/sandbox.mjs';
|
|
221
|
+
export function createAdapter() {
|
|
222
|
+
return createDockerSandboxAdapter();
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Verification
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
node --check src/cloud.mjs
|
|
230
|
+
node --check src/providers/sandbox.mjs
|
|
231
|
+
npm run check
|
|
232
|
+
AGENT_ENV_TEST_SANDBOX=1 node test/run.mjs docker
|
|
233
|
+
# Requires an explicitly selected Hetzner test project and creates billed resources:
|
|
234
|
+
AGENT_ENV_TEST_CLOUD=1 node test/run.mjs cloud
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Set `AGENT_ENV_CLI` to the absolute path of an installed package executable.
|
|
238
|
+
The sandbox suite exercises the public CLI against the real Docker bridge.
|
|
239
|
+
The cloud suite exercises the public CLI against real Hetzner infrastructure and removes its owned resources.
|
|
240
|
+
Ordinary CI does not run the cloud suite. See [CI and releases](../../docs/releases.md).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Provider interface
|
|
2
|
+
|
|
3
|
+
Register an implementation in `index.mjs`. A provider receives a validated specification:
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
{
|
|
7
|
+
provider: 'docker',
|
|
8
|
+
owner: 'installation-hash',
|
|
9
|
+
name: 'instance-name',
|
|
10
|
+
cwd: '/absolute/local/checkout',
|
|
11
|
+
host: 'optional-ssh-alias',
|
|
12
|
+
sshConfig: '/optional/ssh/config',
|
|
13
|
+
services: {
|
|
14
|
+
web: {
|
|
15
|
+
image: 'node:22-bookworm-slim',
|
|
16
|
+
port: 3000,
|
|
17
|
+
containerPort: 3000,
|
|
18
|
+
protocol: 'http',
|
|
19
|
+
command: 'npm run dev',
|
|
20
|
+
env: {},
|
|
21
|
+
volumes: { dependencies: '/workspace/node_modules' }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The returned handle must be JSON serializable. It contains `spec` and `services`.
|
|
28
|
+
Each published service includes its public `port` and `protocol`.
|
|
29
|
+
Compose internal services use `internal: true` and omit routing fields.
|
|
30
|
+
The provider can add container IDs, workspace paths, published ports, or VM IDs.
|
|
31
|
+
Runtime connections stay outside the persisted handle.
|
|
32
|
+
|
|
33
|
+
Implement these methods:
|
|
34
|
+
|
|
35
|
+
| Method | Contract |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `create(spec)` | Allocate resources, start services, wait for readiness, and return a handle. Clean partial allocation on failure. |
|
|
38
|
+
| `start(handle)` | Resume existing resources and wait for readiness. |
|
|
39
|
+
| `wait(handle)` | Check existing resources and refresh their connection information. Do not create new resources. |
|
|
40
|
+
| `connect(handle)` | Return `{name, backendPort, close()}` for each service. The port must accept connections on local loopback. |
|
|
41
|
+
| `stop(handle)` | Stop compute while retaining persistent data. |
|
|
42
|
+
| `remove(handle, purge)` | Remove compute. Remove persistent data only when `purge` is true. |
|
|
43
|
+
| `status(handle)` | Return a map from service names to runtime states. |
|
|
44
|
+
| `sync(handle)` | Synchronize local source files when the runtime needs a copy. |
|
|
45
|
+
| `logs(handle, service)` | Return recent stdout and stderr. |
|
|
46
|
+
| `exec(handle, service, argv)` | Execute arguments without implicit shell interpretation. Return output or throw on failure. |
|
|
47
|
+
| `fork(handle, destinationSpec)` | Copy stopped data and create a destination. Never overwrite existing destination data. |
|
|
48
|
+
|
|
49
|
+
`connect` may include a Node child process as `child` for SSH tunnel status.
|
|
50
|
+
Each `close()` must release only its own connection.
|
|
51
|
+
An optional `dispose()` method closes provider sessions when the daemon stops.
|
|
52
|
+
The daemon owns public listeners, hostname selection, private state, and command serialization.
|
|
53
|
+
Providers own compute, source synchronization, readiness, and persistent volumes.
|
|
54
|
+
|
|
55
|
+
The initial Docker implementation combines compute and local/SSH transport in one class.
|
|
56
|
+
Split transport implementations only when another provider needs them.
|
|
57
|
+
A Firecracker implementation can return the same local connection contract through SSH or a VM network.
|
|
58
|
+
Its host must support KVM. Its service image format can differ, but it must retain the public service contract.
|
|
59
|
+
|
|
60
|
+
The snapshot module exports and restores verified archives locally or through S3.
|
|
61
|
+
Compose supplies `snapshotVolumeNames` and `snapshotVolumeLabels` to map restored volumes.
|
|
62
|
+
The daemon coordinates Compose forks through these snapshot hooks. An object store is not a substitute for a database's active block storage.
|