@madarco/agentbox 0.5.0 → 0.7.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/dist/_cloud-attach-DMVH6GWO.js +12 -0
- package/dist/chunk-7KOEFGN2.js +1162 -0
- package/dist/chunk-7KOEFGN2.js.map +1 -0
- package/dist/chunk-I24B6AXR.js +600 -0
- package/dist/chunk-I24B6AXR.js.map +1 -0
- package/dist/chunk-NAVL4R34.js +7546 -0
- package/dist/chunk-NAVL4R34.js.map +1 -0
- package/dist/chunk-NW5NYTQM.js +1366 -0
- package/dist/chunk-NW5NYTQM.js.map +1 -0
- package/dist/chunk-UK72UQ5U.js +237 -0
- package/dist/chunk-UK72UQ5U.js.map +1 -0
- package/dist/chunk-V5KZGB5V.js +722 -0
- package/dist/chunk-V5KZGB5V.js.map +1 -0
- package/dist/cloud-poller-ZIWSADJB-JXFRJUEM.js +10 -0
- package/dist/dist-ETCFRVPA.js +423 -0
- package/dist/dist-QZGJIBT5.js +1339 -0
- package/dist/dist-QZGJIBT5.js.map +1 -0
- package/dist/dist-R67WMLCF.js +183 -0
- package/dist/dist-R67WMLCF.js.map +1 -0
- package/dist/index.js +4088 -1451
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/runtime/docker/Dockerfile.box +115 -19
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +34 -19
- package/runtime/docker/packages/ctl/dist/bin.cjs +10246 -758
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +13 -3
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +37 -0
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +4 -9
- package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
- package/runtime/hetzner/agentbox-codex-hooks.json +37 -0
- package/runtime/hetzner/agentbox-dockerd-start +132 -0
- package/runtime/hetzner/agentbox-open +28 -0
- package/runtime/hetzner/agentbox-setup-skill.md +196 -0
- package/runtime/hetzner/agentbox-vnc-start +77 -0
- package/runtime/hetzner/claude-managed-settings.json +54 -0
- package/runtime/hetzner/ctl.cjs +22350 -0
- package/runtime/hetzner/custom-system-CLAUDE.md +27 -0
- package/runtime/hetzner/scripts/install-box.sh +365 -0
- package/runtime/relay/bin.cjs +9182 -754
- package/share/agentbox-setup/SKILL.md +34 -19
- package/dist/chunk-6VTAPD4H.js +0 -507
- package/dist/chunk-6VTAPD4H.js.map +0 -1
- package/dist/chunk-7J5AJLWG.js +0 -238
- package/dist/chunk-7J5AJLWG.js.map +0 -1
- package/dist/chunk-FJNIFTWK.js +0 -523
- package/dist/chunk-FJNIFTWK.js.map +0 -1
- package/dist/chunk-HPZMD5DE.js +0 -106
- package/dist/chunk-HPZMD5DE.js.map +0 -1
- package/dist/chunk-PXUBE5KS.js +0 -2346
- package/dist/chunk-PXUBE5KS.js.map +0 -1
- package/dist/chunk-RFC5F5HR.js +0 -1709
- package/dist/chunk-RFC5F5HR.js.map +0 -1
- package/dist/create-AHZ3GVEZ-TGEDL7UX.js +0 -15
- package/dist/lifecycle-LFOL6YFM-TCHDX3J5.js +0 -38
- package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
- package/dist/stats-Z4BVJODD-HEC4TMUZ.js +0 -19
- package/dist/stats-Z4BVJODD-HEC4TMUZ.js.map +0 -1
- /package/dist/{create-AHZ3GVEZ-TGEDL7UX.js.map → _cloud-attach-DMVH6GWO.js.map} +0 -0
- /package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js.map → cloud-poller-ZIWSADJB-JXFRJUEM.js.map} +0 -0
- /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-ETCFRVPA.js.map} +0 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../packages/sandbox-hetzner/dist/chunk-4DD4EISF.js
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
import { spawnSync } from "child_process";
|
|
8
|
+
import {
|
|
9
|
+
chmodSync,
|
|
10
|
+
existsSync as existsSync2,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync as readFileSync2,
|
|
13
|
+
renameSync,
|
|
14
|
+
writeFileSync
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { homedir as homedir2 } from "os";
|
|
17
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
18
|
+
import { confirm, isCancel, intro, log, note, outro, password, spinner } from "@clack/prompts";
|
|
19
|
+
var HETZNER_KEYS = ["HCLOUD_TOKEN", "HCLOUD_ENDPOINT"];
|
|
20
|
+
var loaded = false;
|
|
21
|
+
function ensureHetznerEnvLoaded() {
|
|
22
|
+
if (loaded) return;
|
|
23
|
+
loaded = true;
|
|
24
|
+
importHetznerFromFile(resolve(homedir(), ".agentbox", "secrets.env"));
|
|
25
|
+
}
|
|
26
|
+
function importHetznerFromFile(path) {
|
|
27
|
+
if (!existsSync(path)) return;
|
|
28
|
+
let body;
|
|
29
|
+
try {
|
|
30
|
+
body = readFileSync(path, "utf8");
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const parsed = parseEnvFile(body);
|
|
35
|
+
for (const key of HETZNER_KEYS) {
|
|
36
|
+
if (process.env[key] !== void 0) continue;
|
|
37
|
+
const value = parsed[key];
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
process.env[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function parseEnvFile(body) {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
46
|
+
const line = rawLine.trim();
|
|
47
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
48
|
+
const stripped = line.startsWith("export ") ? line.slice("export ".length) : line;
|
|
49
|
+
const eq = stripped.indexOf("=");
|
|
50
|
+
if (eq <= 0) continue;
|
|
51
|
+
const key = stripped.slice(0, eq).trim();
|
|
52
|
+
let value = stripped.slice(eq + 1).trim();
|
|
53
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
54
|
+
value = value.slice(1, -1);
|
|
55
|
+
}
|
|
56
|
+
out[key] = value;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
var DEFAULT_HCLOUD_ENDPOINT = "https://api.hetzner.cloud/v1";
|
|
61
|
+
var HetznerApiError = class extends Error {
|
|
62
|
+
statusCode;
|
|
63
|
+
code;
|
|
64
|
+
details;
|
|
65
|
+
constructor(statusCode, code, message, details) {
|
|
66
|
+
super(`hetzner ${String(statusCode)} ${code}: ${message}`);
|
|
67
|
+
this.name = "HetznerApiError";
|
|
68
|
+
this.statusCode = statusCode;
|
|
69
|
+
this.code = code;
|
|
70
|
+
this.details = details;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function makeHetznerClient(opts = {}) {
|
|
74
|
+
ensureHetznerEnvLoaded();
|
|
75
|
+
const rawToken = opts.token ?? process.env.HCLOUD_TOKEN;
|
|
76
|
+
if (!rawToken || rawToken.trim().length === 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"Hetzner credentials not configured: HCLOUD_TOKEN is empty.\nRun `agentbox hetzner login` interactively, or set HCLOUD_TOKEN in the environment."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const token = rawToken.trim();
|
|
82
|
+
const endpoint = (opts.endpoint ?? process.env.HCLOUD_ENDPOINT ?? DEFAULT_HCLOUD_ENDPOINT).replace(/\/$/, "");
|
|
83
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
84
|
+
async function req(method, path, body) {
|
|
85
|
+
const url = `${endpoint}${path}`;
|
|
86
|
+
const init = {
|
|
87
|
+
method,
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${token}`,
|
|
90
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
91
|
+
},
|
|
92
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
93
|
+
};
|
|
94
|
+
const res = await fetchImpl(url, init);
|
|
95
|
+
if (res.status === 204) return null;
|
|
96
|
+
if (res.status === 404) return null;
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
let parsed = {};
|
|
99
|
+
try {
|
|
100
|
+
parsed = await res.json();
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
const code = parsed.error?.code ?? `http_${String(res.status)}`;
|
|
104
|
+
const msg = parsed.error?.message ?? res.statusText ?? "unknown error";
|
|
105
|
+
throw new HetznerApiError(res.status, code, msg, parsed.error?.details);
|
|
106
|
+
}
|
|
107
|
+
const text = await res.text();
|
|
108
|
+
if (text.length === 0) return null;
|
|
109
|
+
return JSON.parse(text);
|
|
110
|
+
}
|
|
111
|
+
async function reqExpect(method, path, body) {
|
|
112
|
+
const out = await req(method, path, body);
|
|
113
|
+
if (out === null) {
|
|
114
|
+
throw new HetznerApiError(0, "empty_response", `expected a body from ${method} ${path}`);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
async getServer(id) {
|
|
120
|
+
const r = await req("GET", `/servers/${String(id)}`);
|
|
121
|
+
return r?.server ?? null;
|
|
122
|
+
},
|
|
123
|
+
async createServer(reqBody) {
|
|
124
|
+
const r = await reqExpect(
|
|
125
|
+
"POST",
|
|
126
|
+
"/servers",
|
|
127
|
+
reqBody
|
|
128
|
+
);
|
|
129
|
+
return { server: r.server, action: r.action };
|
|
130
|
+
},
|
|
131
|
+
async listServers(opts2) {
|
|
132
|
+
const params = new URLSearchParams();
|
|
133
|
+
if (opts2?.label_selector) params.set("label_selector", opts2.label_selector);
|
|
134
|
+
params.set("per_page", "50");
|
|
135
|
+
const all = [];
|
|
136
|
+
let pageNum = 1;
|
|
137
|
+
while (true) {
|
|
138
|
+
params.set("page", String(pageNum));
|
|
139
|
+
const r = await reqExpect("GET", `/servers?${params.toString()}`);
|
|
140
|
+
all.push(...r.servers);
|
|
141
|
+
const next = r.meta?.pagination?.next_page;
|
|
142
|
+
if (typeof next !== "number") break;
|
|
143
|
+
pageNum = next;
|
|
144
|
+
}
|
|
145
|
+
return all;
|
|
146
|
+
},
|
|
147
|
+
async deleteServer(id) {
|
|
148
|
+
const r = await req("DELETE", `/servers/${String(id)}`);
|
|
149
|
+
return r?.action ?? null;
|
|
150
|
+
},
|
|
151
|
+
async powerOn(id) {
|
|
152
|
+
const r = await reqExpect(
|
|
153
|
+
"POST",
|
|
154
|
+
`/servers/${String(id)}/actions/poweron`
|
|
155
|
+
);
|
|
156
|
+
return r.action;
|
|
157
|
+
},
|
|
158
|
+
async powerOff(id) {
|
|
159
|
+
const r = await reqExpect(
|
|
160
|
+
"POST",
|
|
161
|
+
`/servers/${String(id)}/actions/poweroff`
|
|
162
|
+
);
|
|
163
|
+
return r.action;
|
|
164
|
+
},
|
|
165
|
+
async shutdown(id) {
|
|
166
|
+
const r = await reqExpect(
|
|
167
|
+
"POST",
|
|
168
|
+
`/servers/${String(id)}/actions/shutdown`
|
|
169
|
+
);
|
|
170
|
+
return r.action;
|
|
171
|
+
},
|
|
172
|
+
async createImage(id, body) {
|
|
173
|
+
const r = await reqExpect(
|
|
174
|
+
"POST",
|
|
175
|
+
`/servers/${String(id)}/actions/create_image`,
|
|
176
|
+
body
|
|
177
|
+
);
|
|
178
|
+
return { image: r.image, action: r.action };
|
|
179
|
+
},
|
|
180
|
+
async getImage(id) {
|
|
181
|
+
const r = await req("GET", `/images/${String(id)}`);
|
|
182
|
+
return r?.image ?? null;
|
|
183
|
+
},
|
|
184
|
+
async listImages(opts2) {
|
|
185
|
+
const params = new URLSearchParams();
|
|
186
|
+
if (opts2?.type) params.set("type", opts2.type);
|
|
187
|
+
if (opts2?.label_selector) params.set("label_selector", opts2.label_selector);
|
|
188
|
+
if (opts2?.name) params.set("name", opts2.name);
|
|
189
|
+
params.set("per_page", "50");
|
|
190
|
+
const all = [];
|
|
191
|
+
let pageNum = 1;
|
|
192
|
+
while (true) {
|
|
193
|
+
params.set("page", String(pageNum));
|
|
194
|
+
const r = await reqExpect("GET", `/images?${params.toString()}`);
|
|
195
|
+
all.push(...r.images);
|
|
196
|
+
const next = r.meta?.pagination?.next_page;
|
|
197
|
+
if (typeof next !== "number") break;
|
|
198
|
+
pageNum = next;
|
|
199
|
+
}
|
|
200
|
+
return all;
|
|
201
|
+
},
|
|
202
|
+
async deleteImage(id) {
|
|
203
|
+
await req("DELETE", `/images/${String(id)}`);
|
|
204
|
+
},
|
|
205
|
+
async createFirewall(reqBody) {
|
|
206
|
+
const r = await reqExpect("POST", "/firewalls", reqBody);
|
|
207
|
+
return r.firewall;
|
|
208
|
+
},
|
|
209
|
+
async setFirewallRules(id, rules) {
|
|
210
|
+
const r = await reqExpect(
|
|
211
|
+
"POST",
|
|
212
|
+
`/firewalls/${String(id)}/actions/set_rules`,
|
|
213
|
+
{ rules }
|
|
214
|
+
);
|
|
215
|
+
return r.actions;
|
|
216
|
+
},
|
|
217
|
+
async getFirewall(id) {
|
|
218
|
+
const r = await req("GET", `/firewalls/${String(id)}`);
|
|
219
|
+
return r?.firewall ?? null;
|
|
220
|
+
},
|
|
221
|
+
async deleteFirewall(id) {
|
|
222
|
+
await req("DELETE", `/firewalls/${String(id)}`);
|
|
223
|
+
},
|
|
224
|
+
async listLocations() {
|
|
225
|
+
const r = await reqExpect("GET", "/locations");
|
|
226
|
+
return r.locations;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
var DASHBOARD_KEYS_URL = "https://console.hetzner.cloud/projects";
|
|
231
|
+
var MANAGED_KEYS = ["HCLOUD_TOKEN", "HCLOUD_ENDPOINT"];
|
|
232
|
+
async function ensureHetznerCredentials(opts = {}) {
|
|
233
|
+
ensureHetznerEnvLoaded();
|
|
234
|
+
if (!opts.force && hasUsableCredentials()) return;
|
|
235
|
+
if (!process.stdin.isTTY) return;
|
|
236
|
+
intro("Hetzner Cloud setup");
|
|
237
|
+
note(
|
|
238
|
+
`AgentBox needs a Hetzner Cloud API token (project-scoped) to provision VPSes.
|
|
239
|
+
|
|
240
|
+
1. Open ${DASHBOARD_KEYS_URL}
|
|
241
|
+
2. Pick a project (or create one).
|
|
242
|
+
3. Security \u2192 API Tokens \u2192 Generate API Token (Read + Write).`,
|
|
243
|
+
"API token required"
|
|
244
|
+
);
|
|
245
|
+
const open = await confirm({
|
|
246
|
+
message: `Open ${DASHBOARD_KEYS_URL} in your browser?`,
|
|
247
|
+
initialValue: true
|
|
248
|
+
});
|
|
249
|
+
if (isCancel(open)) {
|
|
250
|
+
log.warn("Hetzner setup cancelled \u2014 re-run `agentbox hetzner login` when ready.");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (open) openDashboard();
|
|
254
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
255
|
+
const creds = await promptForCredentials();
|
|
256
|
+
if (creds === null) return;
|
|
257
|
+
const result = await validateCredentials(creds);
|
|
258
|
+
if (result.ok) {
|
|
259
|
+
persistCredentials(creds);
|
|
260
|
+
log.success(`Hetzner credentials saved to ${secretsPath()}`);
|
|
261
|
+
outro("Setup complete.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (result.kind === "auth" && attempt === 0) {
|
|
265
|
+
log.error(`That token was rejected by Hetzner: ${result.message}`);
|
|
266
|
+
log.info("Try again, or press Ctrl-C to cancel.");
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (result.kind === "network") {
|
|
270
|
+
log.warn(`Could not reach Hetzner to validate (${result.message}) \u2014 saving anyway.`);
|
|
271
|
+
persistCredentials(creds);
|
|
272
|
+
log.success(`Hetzner credentials saved to ${secretsPath()}`);
|
|
273
|
+
outro("Setup complete (unvalidated).");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
throw new Error(`Hetzner credentials rejected: ${result.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function hasUsableCredentials() {
|
|
280
|
+
return typeof process.env.HCLOUD_TOKEN === "string" && process.env.HCLOUD_TOKEN.length > 0;
|
|
281
|
+
}
|
|
282
|
+
async function promptForCredentials() {
|
|
283
|
+
const token = await password({
|
|
284
|
+
message: "Paste your Hetzner Cloud API token",
|
|
285
|
+
validate(v) {
|
|
286
|
+
if (!v || v.trim().length === 0) return "Cannot be empty";
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
if (isCancel(token)) {
|
|
291
|
+
log.warn("Hetzner setup cancelled.");
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return { token: token.trim() };
|
|
295
|
+
}
|
|
296
|
+
async function validateCredentials(creds) {
|
|
297
|
+
const s = spinner();
|
|
298
|
+
s.start("Validating credentials with Hetzner");
|
|
299
|
+
try {
|
|
300
|
+
const client = makeHetznerClient({ token: creds.token, endpoint: creds.endpoint });
|
|
301
|
+
await client.listLocations();
|
|
302
|
+
s.stop("Hetzner credentials accepted");
|
|
303
|
+
return { ok: true };
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
306
|
+
s.stop("Hetzner credentials check failed");
|
|
307
|
+
if (/401|403|unauthor|forbidden|invalid|token/i.test(message)) {
|
|
308
|
+
return { ok: false, kind: "auth", message };
|
|
309
|
+
}
|
|
310
|
+
return { ok: false, kind: "network", message };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function persistCredentials(creds) {
|
|
314
|
+
process.env.HCLOUD_TOKEN = creds.token;
|
|
315
|
+
if (creds.endpoint) process.env.HCLOUD_ENDPOINT = creds.endpoint;
|
|
316
|
+
const path = secretsPath();
|
|
317
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
318
|
+
let existing = "";
|
|
319
|
+
if (existsSync2(path)) {
|
|
320
|
+
try {
|
|
321
|
+
existing = readFileSync2(path, "utf8");
|
|
322
|
+
} catch {
|
|
323
|
+
existing = "";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const kept = existing.split(/\r?\n/).filter((line) => {
|
|
327
|
+
const stripped = line.startsWith("export ") ? line.slice("export ".length) : line;
|
|
328
|
+
const eq = stripped.indexOf("=");
|
|
329
|
+
if (eq <= 0) return true;
|
|
330
|
+
const key = stripped.slice(0, eq).trim();
|
|
331
|
+
return !MANAGED_KEYS.includes(key);
|
|
332
|
+
}).join("\n").replace(/\s+$/u, "");
|
|
333
|
+
const lines = [`HCLOUD_TOKEN=${creds.token}`];
|
|
334
|
+
if (creds.endpoint) lines.push(`HCLOUD_ENDPOINT=${creds.endpoint}`);
|
|
335
|
+
const body = (kept ? `${kept}
|
|
336
|
+
` : "") + lines.join("\n") + "\n";
|
|
337
|
+
const tmp = `${path}.tmp`;
|
|
338
|
+
writeFileSync(tmp, body, { mode: 384 });
|
|
339
|
+
try {
|
|
340
|
+
chmodSync(tmp, 384);
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
renameSync(tmp, path);
|
|
344
|
+
try {
|
|
345
|
+
chmodSync(path, 384);
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function openDashboard() {
|
|
350
|
+
try {
|
|
351
|
+
const r = spawnSync("open", [DASHBOARD_KEYS_URL], { stdio: "ignore" });
|
|
352
|
+
if (r.status !== 0) {
|
|
353
|
+
log.warn(`Could not auto-open the browser \u2014 visit ${DASHBOARD_KEYS_URL} manually.`);
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
log.warn(`Could not auto-open the browser \u2014 visit ${DASHBOARD_KEYS_URL} manually.`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function secretsPath() {
|
|
360
|
+
return resolve2(homedir2(), ".agentbox", "secrets.env");
|
|
361
|
+
}
|
|
362
|
+
function readHetznerCredStatus() {
|
|
363
|
+
const shellHadToken = !!process.env.HCLOUD_TOKEN;
|
|
364
|
+
ensureHetznerEnvLoaded();
|
|
365
|
+
const token = process.env.HCLOUD_TOKEN;
|
|
366
|
+
const endpoint = process.env.HCLOUD_ENDPOINT;
|
|
367
|
+
if (!token) return { source: "none" };
|
|
368
|
+
return {
|
|
369
|
+
token,
|
|
370
|
+
endpoint,
|
|
371
|
+
source: shellHadToken ? "env" : "secrets.env"
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function maskKey(value) {
|
|
375
|
+
if (value.length <= 8) return "*".repeat(value.length);
|
|
376
|
+
return `${value.slice(0, 4)}\u2026${"*".repeat(8)}${value.slice(-4)}`;
|
|
377
|
+
}
|
|
378
|
+
var PROBES = [
|
|
379
|
+
"https://api.ipify.org",
|
|
380
|
+
"https://ifconfig.io/ip",
|
|
381
|
+
"https://icanhazip.com"
|
|
382
|
+
];
|
|
383
|
+
var TIMEOUT_MS = 3e3;
|
|
384
|
+
var IPV4_RE = /^(?:\d{1,3}\.){3}\d{1,3}$/;
|
|
385
|
+
var IPV6_RE = /^[0-9a-fA-F:]+$/;
|
|
386
|
+
async function detectEgressIp(opts = {}) {
|
|
387
|
+
const probes = opts.probes ?? PROBES;
|
|
388
|
+
const timeout = opts.timeoutMs ?? TIMEOUT_MS;
|
|
389
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
390
|
+
const errors = [];
|
|
391
|
+
for (const url of probes) {
|
|
392
|
+
try {
|
|
393
|
+
const ip = await raceTimeout(probe(url, fetchImpl), timeout);
|
|
394
|
+
if (ip) {
|
|
395
|
+
opts.onLog?.(`egress-ip: detected ${ip} via ${url}`);
|
|
396
|
+
return ip;
|
|
397
|
+
}
|
|
398
|
+
errors.push(`${url}: empty/invalid response`);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
throw new Error(
|
|
404
|
+
`could not auto-detect the host's egress IP \u2014 all ${String(probes.length)} probes failed:
|
|
405
|
+
` + errors.map((e) => ` - ${e}`).join("\n") + `
|
|
406
|
+
Override with --firewall-source <cidr> (e.g. --firewall-source 0.0.0.0/0 for the explicit-open opt-in).`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
async function probe(url, fetchImpl) {
|
|
410
|
+
const res = await fetchImpl(url, { method: "GET" });
|
|
411
|
+
if (!res.ok) return null;
|
|
412
|
+
const body = (await res.text()).trim();
|
|
413
|
+
if (IPV4_RE.test(body)) {
|
|
414
|
+
const parts = body.split(".").map((p) => Number.parseInt(p, 10));
|
|
415
|
+
if (parts.every((p) => p >= 0 && p <= 255)) return body;
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
if (IPV6_RE.test(body) && body.includes(":")) return body;
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
async function raceTimeout(p, ms) {
|
|
422
|
+
let timer;
|
|
423
|
+
try {
|
|
424
|
+
return await Promise.race([
|
|
425
|
+
p,
|
|
426
|
+
new Promise((_resolve, reject) => {
|
|
427
|
+
timer = setTimeout(() => reject(new Error(`probe timed out after ${String(ms)}ms`)), ms);
|
|
428
|
+
})
|
|
429
|
+
]);
|
|
430
|
+
} finally {
|
|
431
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
var DEFAULT_BACKOFF = [1e3, 2e3, 4e3];
|
|
435
|
+
var DEFAULT_ATTEMPT_TIMEOUT_MS = 3e4;
|
|
436
|
+
var AttemptTimeoutError = class extends Error {
|
|
437
|
+
constructor(method, ms) {
|
|
438
|
+
super(`hetzner ${method}: per-attempt timeout after ${String(ms)}ms`);
|
|
439
|
+
this.name = "AttemptTimeoutError";
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
function isAttemptTimeout(err) {
|
|
443
|
+
return err instanceof AttemptTimeoutError;
|
|
444
|
+
}
|
|
445
|
+
function isRetriable(err, allowAmbiguous) {
|
|
446
|
+
if (err instanceof HetznerApiError) {
|
|
447
|
+
if (err.statusCode === 429 || err.code === "rate_limit_exceeded") return true;
|
|
448
|
+
if (err.statusCode >= 500 && err.statusCode <= 599) return allowAmbiguous;
|
|
449
|
+
if (err.code === "locked" || err.code === "conflict") return true;
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
if (err instanceof AttemptTimeoutError) return allowAmbiguous;
|
|
453
|
+
if (err && typeof err === "object") {
|
|
454
|
+
const candidates = [err, err.cause];
|
|
455
|
+
for (const c of candidates) {
|
|
456
|
+
if (!c || typeof c !== "object") continue;
|
|
457
|
+
const code = c.code;
|
|
458
|
+
if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ECONNABORTED" || code === "EAI_AGAIN" || code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "UND_ERR_SOCKET" || code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
459
|
+
return allowAmbiguous;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
async function withHetznerRetry(opts, fn) {
|
|
466
|
+
const backoff = opts.backoffMs ?? DEFAULT_BACKOFF;
|
|
467
|
+
const maxAttempts = backoff.length + 1;
|
|
468
|
+
const timeoutMs = opts.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS;
|
|
469
|
+
const log2 = opts.onRetry ?? defaultRetryLog;
|
|
470
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
471
|
+
try {
|
|
472
|
+
return await raceTimeout2(fn(), timeoutMs, opts.method);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
const last = attempt === maxAttempts;
|
|
475
|
+
if (last || !isRetriable(err, opts.retryOnAmbiguous)) throw err;
|
|
476
|
+
const delay = backoff[attempt - 1] ?? backoff[backoff.length - 1] ?? 4e3;
|
|
477
|
+
log2(
|
|
478
|
+
`hetzner ${opts.method}: attempt ${String(attempt)} failed (${errorSummary(err)}); retrying in ${String(delay)}ms`
|
|
479
|
+
);
|
|
480
|
+
await sleep(delay);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
throw new Error(`withHetznerRetry: exhausted attempts for ${opts.method}`);
|
|
484
|
+
}
|
|
485
|
+
function defaultRetryLog(line) {
|
|
486
|
+
process.stderr.write(`
|
|
487
|
+
[hetzner-retry] ${line}
|
|
488
|
+
`);
|
|
489
|
+
}
|
|
490
|
+
function sleep(ms) {
|
|
491
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
492
|
+
}
|
|
493
|
+
async function raceTimeout2(p, ms, method) {
|
|
494
|
+
let timer;
|
|
495
|
+
try {
|
|
496
|
+
return await Promise.race([
|
|
497
|
+
p,
|
|
498
|
+
new Promise((_resolve, reject) => {
|
|
499
|
+
timer = setTimeout(() => reject(new AttemptTimeoutError(method, ms)), ms);
|
|
500
|
+
})
|
|
501
|
+
]);
|
|
502
|
+
} finally {
|
|
503
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function errorSummary(err) {
|
|
507
|
+
if (err instanceof HetznerApiError) {
|
|
508
|
+
return `HetznerApiError ${String(err.statusCode)} ${err.code}: ${truncate(err.message)}`;
|
|
509
|
+
}
|
|
510
|
+
if (err instanceof Error) {
|
|
511
|
+
const code = err.code;
|
|
512
|
+
return code !== void 0 ? `${err.name}(${String(code)}): ${truncate(err.message)}` : `${err.name}: ${truncate(err.message)}`;
|
|
513
|
+
}
|
|
514
|
+
return truncate(String(err));
|
|
515
|
+
}
|
|
516
|
+
function truncate(s, max = 160) {
|
|
517
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
518
|
+
}
|
|
519
|
+
function sshOnlyInboundRule(sourceCidr) {
|
|
520
|
+
return [
|
|
521
|
+
{
|
|
522
|
+
direction: "in",
|
|
523
|
+
protocol: "tcp",
|
|
524
|
+
port: "22",
|
|
525
|
+
source_ips: [sourceCidr],
|
|
526
|
+
description: "agentbox: SSH from host egress IP only"
|
|
527
|
+
}
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
async function createPerBoxFirewall(client, opts) {
|
|
531
|
+
return withHetznerRetry(
|
|
532
|
+
{ method: "createFirewall", retryOnAmbiguous: false, attemptTimeoutMs: 6e4 },
|
|
533
|
+
() => client.createFirewall({
|
|
534
|
+
name: opts.name,
|
|
535
|
+
rules: sshOnlyInboundRule(opts.sourceCidr),
|
|
536
|
+
labels: {
|
|
537
|
+
"agentbox.managed": "true",
|
|
538
|
+
"agentbox.role": "box",
|
|
539
|
+
...opts.labels
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
async function syncFirewallSource(client, firewallId, sourceCidr) {
|
|
545
|
+
await withHetznerRetry(
|
|
546
|
+
{ method: "setFirewallRules", retryOnAmbiguous: true, attemptTimeoutMs: 6e4 },
|
|
547
|
+
() => client.setFirewallRules(firewallId, sshOnlyInboundRule(sourceCidr))
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
async function deletePerBoxFirewall(client, firewallId, opts = {}) {
|
|
551
|
+
const deadline = Date.now() + (opts.detachWaitMs ?? 6e4);
|
|
552
|
+
let interval = 1e3;
|
|
553
|
+
while (true) {
|
|
554
|
+
try {
|
|
555
|
+
await withHetznerRetry(
|
|
556
|
+
{ method: "deleteFirewall", retryOnAmbiguous: true, attemptTimeoutMs: 3e4 },
|
|
557
|
+
() => client.deleteFirewall(firewallId)
|
|
558
|
+
);
|
|
559
|
+
return;
|
|
560
|
+
} catch (err) {
|
|
561
|
+
if (err instanceof HetznerApiError && (err.statusCode === 404 || err.code === "not_found")) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const stillAttached = err instanceof HetznerApiError && (err.statusCode === 409 || err.code === "conflict" || err.code === "resource_in_use");
|
|
565
|
+
if (stillAttached && Date.now() < deadline) {
|
|
566
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
567
|
+
interval = Math.min(interval * 2, 8e3);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
throw err;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function normalizeSourceCidr(raw) {
|
|
575
|
+
const trimmed = raw.trim();
|
|
576
|
+
if (trimmed.includes("/")) return trimmed;
|
|
577
|
+
if (trimmed.includes(":")) return `${trimmed}/128`;
|
|
578
|
+
return `${trimmed}/32`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export {
|
|
582
|
+
ensureHetznerEnvLoaded,
|
|
583
|
+
DEFAULT_HCLOUD_ENDPOINT,
|
|
584
|
+
HetznerApiError,
|
|
585
|
+
makeHetznerClient,
|
|
586
|
+
ensureHetznerCredentials,
|
|
587
|
+
secretsPath,
|
|
588
|
+
readHetznerCredStatus,
|
|
589
|
+
maskKey,
|
|
590
|
+
detectEgressIp,
|
|
591
|
+
isAttemptTimeout,
|
|
592
|
+
isRetriable,
|
|
593
|
+
withHetznerRetry,
|
|
594
|
+
sshOnlyInboundRule,
|
|
595
|
+
createPerBoxFirewall,
|
|
596
|
+
syncFirewallSource,
|
|
597
|
+
deletePerBoxFirewall,
|
|
598
|
+
normalizeSourceCidr
|
|
599
|
+
};
|
|
600
|
+
//# sourceMappingURL=chunk-I24B6AXR.js.map
|