@sellable/install 0.1.358 → 0.1.359-phase111.20260720052618
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/bin/sellable-install.mjs +0 -2
- package/package.json +2 -11
- package/skill-templates/refill-sends-evergreen.md +5 -0
- package/skill-templates/refill-sends-v2.md +23 -1
- package/skill-templates/refill-sends.md +14 -1
- package/bin/sellable-agent-egress-proxy.mjs +0 -31
- package/bin/sellable-agent-host-bootstrap.mjs +0 -26
- package/bin/sellable-agent-host-worker.mjs +0 -9
- package/bin/sellable-agent-runtime-helper.mjs +0 -37
- package/bin/sellable-agent-runtime-launcher.mjs +0 -151
- package/bin/sellable-agent-runtime-probe.mjs +0 -163
- package/bin/sellable-agent-sandbox-init.mjs +0 -93
- package/container/Dockerfile +0 -37
- package/container/README.md +0 -15
- package/container/compose.yaml +0 -22
- package/container/entrypoint.sh +0 -13
- package/lib/sellable-agent/containment-contract.mjs +0 -472
- package/lib/sellable-agent/external-runtime-builder.mjs +0 -272
- package/lib/sellable-agent/hermes-bridge.mjs +0 -497
- package/lib/sellable-agent/host-bootstrap.mjs +0 -458
- package/lib/sellable-agent/host-worker.mjs +0 -2067
- package/lib/sellable-agent/profile-materializer.mjs +0 -1410
- package/lib/sellable-agent/provisioning-adapter.mjs +0 -992
- package/lib/sellable-agent/runtime-boundary.mjs +0 -635
- package/lib/sellable-agent/runtime-egress-proxy.mjs +0 -241
- package/lib/sellable-agent/runtime-helper.mjs +0 -1428
- package/lib/sellable-agent/runtime-reconciler.mjs +0 -683
- package/lib/sellable-agent/service-installer.mjs +0 -441
- package/services/s6/sellable-agent-egress-proxy/run +0 -15
- package/services/s6/sellable-agent-host-worker/run +0 -4
- package/services/s6/sellable-agent-runtime/run +0 -19
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { promises as dns } from "node:dns";
|
|
3
|
-
import { createServer as createHttpServer } from "node:http";
|
|
4
|
-
import { isIP } from "node:net";
|
|
5
|
-
import { createConnection } from "node:net";
|
|
6
|
-
import { domainToASCII } from "node:url";
|
|
7
|
-
import { lstatSync, readFileSync, readlinkSync } from "node:fs";
|
|
8
|
-
import { isAbsolute, resolve } from "node:path";
|
|
9
|
-
import { RUNTIME_EGRESS_HOSTS } from "./runtime-boundary.mjs";
|
|
10
|
-
|
|
11
|
-
export const RUNTIME_EGRESS_PROXY_VERSION = "sellable-agent-egress-proxy/v1";
|
|
12
|
-
const CONFIG_KEYS = new Set(["version", "listenHost", "listenPort", "uid", "gid", "allowedHosts"]);
|
|
13
|
-
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
14
|
-
|
|
15
|
-
function stable(value) {
|
|
16
|
-
if (Array.isArray(value)) return value.map(stable);
|
|
17
|
-
if (value && typeof value === "object") {
|
|
18
|
-
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
19
|
-
}
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function validateRuntimeEgressProxyConfig(value) {
|
|
24
|
-
if (
|
|
25
|
-
!value ||
|
|
26
|
-
typeof value !== "object" ||
|
|
27
|
-
Array.isArray(value) ||
|
|
28
|
-
Object.keys(value).length !== CONFIG_KEYS.size ||
|
|
29
|
-
Object.keys(value).some((key) => !CONFIG_KEYS.has(key)) ||
|
|
30
|
-
value.version !== RUNTIME_EGRESS_PROXY_VERSION ||
|
|
31
|
-
value.listenHost !== "127.0.0.1" ||
|
|
32
|
-
!Number.isSafeInteger(value.listenPort) ||
|
|
33
|
-
value.listenPort < 1024 ||
|
|
34
|
-
value.listenPort > 65535 ||
|
|
35
|
-
!Number.isSafeInteger(value.uid) ||
|
|
36
|
-
value.uid < 1 ||
|
|
37
|
-
!Number.isSafeInteger(value.gid) ||
|
|
38
|
-
value.gid < 1 ||
|
|
39
|
-
JSON.stringify(value.allowedHosts) !== JSON.stringify([...RUNTIME_EGRESS_HOSTS])
|
|
40
|
-
) return { ok: false, code: "egress_proxy_config_rejected" };
|
|
41
|
-
const config = Object.freeze({ ...value, allowedHosts: Object.freeze([...value.allowedHosts]) });
|
|
42
|
-
return { ok: true, config, configDigest: sha256(JSON.stringify(stable(config))) };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function normalizedHost(value) {
|
|
46
|
-
if (typeof value !== "string" || !value || value.length > 253 || /[\0\s/@?#]/.test(value)) return null;
|
|
47
|
-
const ascii = domainToASCII(value).toLowerCase();
|
|
48
|
-
if (!ascii || ascii.endsWith(".") || isIP(ascii) || !/^[a-z0-9.-]+$/.test(ascii)) return null;
|
|
49
|
-
if (ascii.split(".").some((label) => !label || label.length > 63 || label.startsWith("-") || label.endsWith("-"))) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return ascii;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function hostAllowed(host, allowedHosts) {
|
|
56
|
-
return allowedHosts.some((entry) =>
|
|
57
|
-
entry.startsWith(".") ? host.endsWith(entry) && host.length > entry.length : host === entry
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function authorizeRuntimeEgressTarget(config, authority) {
|
|
62
|
-
const validated = validateRuntimeEgressProxyConfig(config);
|
|
63
|
-
if (!validated.ok || typeof authority !== "string" || authority.length > 300) {
|
|
64
|
-
return { ok: false, code: "egress_target_rejected" };
|
|
65
|
-
}
|
|
66
|
-
const match = authority.match(/^([^:\[\]]+):(\d{1,5})$/);
|
|
67
|
-
if (!match) return { ok: false, code: "egress_target_rejected" };
|
|
68
|
-
const host = normalizedHost(match[1]);
|
|
69
|
-
const port = Number(match[2]);
|
|
70
|
-
if (!host || port !== 443 || !hostAllowed(host, validated.config.allowedHosts)) {
|
|
71
|
-
return { ok: false, code: "egress_target_rejected" };
|
|
72
|
-
}
|
|
73
|
-
return { ok: true, host, port };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function publicIpv4(address) {
|
|
77
|
-
const parts = address.split(".").map(Number);
|
|
78
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
|
79
|
-
const [a, b, c] = parts;
|
|
80
|
-
if (
|
|
81
|
-
a === 0 || a === 10 || a === 127 || a >= 224 ||
|
|
82
|
-
(a === 100 && b >= 64 && b <= 127) ||
|
|
83
|
-
(a === 169 && b === 254) ||
|
|
84
|
-
(a === 172 && b >= 16 && b <= 31) ||
|
|
85
|
-
(a === 192 && b === 0 && c === 0) ||
|
|
86
|
-
(a === 192 && b === 0 && c === 2) ||
|
|
87
|
-
(a === 192 && b === 168) ||
|
|
88
|
-
(a === 198 && (b === 18 || b === 19)) ||
|
|
89
|
-
(a === 198 && b === 51 && c === 100) ||
|
|
90
|
-
(a === 203 && b === 0 && c === 113)
|
|
91
|
-
) return false;
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function publicIpv6(address) {
|
|
96
|
-
const normalized = address.toLowerCase().split("%")[0];
|
|
97
|
-
return !(
|
|
98
|
-
normalized === "::" ||
|
|
99
|
-
normalized === "::1" ||
|
|
100
|
-
normalized.startsWith("fc") ||
|
|
101
|
-
normalized.startsWith("fd") ||
|
|
102
|
-
/^fe[89ab]/.test(normalized) ||
|
|
103
|
-
normalized.startsWith("ff") ||
|
|
104
|
-
normalized.startsWith("2001:db8:") ||
|
|
105
|
-
normalized.startsWith("::ffff:")
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function isPublicRuntimeEgressAddress(address) {
|
|
110
|
-
const family = isIP(address);
|
|
111
|
-
return family === 4 ? publicIpv4(address) : family === 6 ? publicIpv6(address) : false;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function rejectConnect(socket, status = "403 Forbidden") {
|
|
115
|
-
if (!socket.destroyed) socket.end(`HTTP/1.1 ${status}\r\nConnection: close\r\n\r\n`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function createRuntimeEgressProxy(config, dependencies = {}) {
|
|
119
|
-
const validated = validateRuntimeEgressProxyConfig(config);
|
|
120
|
-
if (!validated.ok) throw new Error("egress proxy config rejected");
|
|
121
|
-
const lookup = dependencies.lookup ?? ((host) => dns.lookup(host, { all: true, verbatim: true, family: 4 }));
|
|
122
|
-
const dial = dependencies.dial ?? ((options) => createConnection(options));
|
|
123
|
-
const maxConnections = 256;
|
|
124
|
-
let connections = 0;
|
|
125
|
-
const server = createHttpServer({ maxHeaderSize: 8 * 1024 }, (_request, response) => {
|
|
126
|
-
response.writeHead(405, { Connection: "close", "Content-Length": "0" });
|
|
127
|
-
response.end();
|
|
128
|
-
});
|
|
129
|
-
server.on("connect", async (request, client, head) => {
|
|
130
|
-
const target = authorizeRuntimeEgressTarget(validated.config, request.url ?? "");
|
|
131
|
-
if (!target.ok || connections >= maxConnections) {
|
|
132
|
-
rejectConnect(client, connections >= maxConnections ? "429 Too Many Requests" : "403 Forbidden");
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
connections += 1;
|
|
136
|
-
let upstream;
|
|
137
|
-
const release = () => { connections = Math.max(0, connections - 1); };
|
|
138
|
-
try {
|
|
139
|
-
const answers = await lookup(target.host);
|
|
140
|
-
const selected = Array.isArray(answers) ? answers[0] : null;
|
|
141
|
-
if (
|
|
142
|
-
!Array.isArray(answers) ||
|
|
143
|
-
!selected ||
|
|
144
|
-
answers.some((answer) => answer.family !== 4 || !publicIpv4(answer.address))
|
|
145
|
-
) {
|
|
146
|
-
throw new Error("egress DNS answer rejected");
|
|
147
|
-
}
|
|
148
|
-
upstream = dial({ host: selected.address, port: target.port, family: selected.family });
|
|
149
|
-
upstream.setTimeout?.(300_000, () => upstream.destroy());
|
|
150
|
-
upstream.once("connect", () => {
|
|
151
|
-
client.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: sellable-agent\r\n\r\n");
|
|
152
|
-
if (head?.length) upstream.write(head);
|
|
153
|
-
client.pipe(upstream);
|
|
154
|
-
upstream.pipe(client);
|
|
155
|
-
});
|
|
156
|
-
upstream.once("error", () => rejectConnect(client, "502 Bad Gateway"));
|
|
157
|
-
upstream.once("close", release);
|
|
158
|
-
client.once("error", () => upstream?.destroy());
|
|
159
|
-
client.once("close", () => upstream?.destroy());
|
|
160
|
-
} catch {
|
|
161
|
-
release();
|
|
162
|
-
rejectConnect(client, "502 Bad Gateway");
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
server.on("clientError", (_error, socket) => rejectConnect(socket, "400 Bad Request"));
|
|
166
|
-
server.maxConnections = maxConnections;
|
|
167
|
-
return {
|
|
168
|
-
server,
|
|
169
|
-
listen() {
|
|
170
|
-
return new Promise((resolveListen, rejectListen) => {
|
|
171
|
-
const onError = (error) => rejectListen(error);
|
|
172
|
-
server.once("error", onError);
|
|
173
|
-
server.listen(validated.config.listenPort, validated.config.listenHost, () => {
|
|
174
|
-
server.off("error", onError);
|
|
175
|
-
resolveListen({
|
|
176
|
-
ok: true,
|
|
177
|
-
version: RUNTIME_EGRESS_PROXY_VERSION,
|
|
178
|
-
host: validated.config.listenHost,
|
|
179
|
-
port: validated.config.listenPort,
|
|
180
|
-
allowedHosts: [...validated.config.allowedHosts],
|
|
181
|
-
configDigest: validated.configDigest,
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
},
|
|
186
|
-
close() {
|
|
187
|
-
return new Promise((resolveClose, rejectClose) => server.close((error) => error ? rejectClose(error) : resolveClose()));
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function parseStatus(source) {
|
|
193
|
-
return Object.fromEntries(source.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
194
|
-
const index = line.indexOf(":");
|
|
195
|
-
return [line.slice(0, index), line.slice(index + 1).trim()];
|
|
196
|
-
}));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function observeRuntimeEgressProxy({ configPath, executablePath, pid, procRoot = "/proc", listenObserved }) {
|
|
200
|
-
try {
|
|
201
|
-
if (
|
|
202
|
-
!isAbsolute(configPath) || resolve(configPath) !== configPath ||
|
|
203
|
-
!isAbsolute(executablePath) || resolve(executablePath) !== executablePath ||
|
|
204
|
-
!Number.isSafeInteger(pid) || pid < 2 || listenObserved !== true
|
|
205
|
-
) throw new Error("proxy observation input rejected");
|
|
206
|
-
const configLink = lstatSync(configPath);
|
|
207
|
-
const executableLink = lstatSync(executablePath);
|
|
208
|
-
if (
|
|
209
|
-
configLink.isSymbolicLink() || !configLink.isFile() || (configLink.mode & 0o022) !== 0 ||
|
|
210
|
-
executableLink.isSymbolicLink() || !executableLink.isFile() || (executableLink.mode & 0o022) !== 0
|
|
211
|
-
) throw new Error("proxy observation file rejected");
|
|
212
|
-
const configBytes = readFileSync(configPath);
|
|
213
|
-
const validated = validateRuntimeEgressProxyConfig(JSON.parse(configBytes.toString("utf8")));
|
|
214
|
-
if (!validated.ok) throw new Error("proxy config rejected");
|
|
215
|
-
const processRoot = resolve(procRoot, String(pid));
|
|
216
|
-
const status = parseStatus(readFileSync(resolve(processRoot, "status"), "utf8"));
|
|
217
|
-
const uid = Number(status.Uid?.split(/\s+/)[0]);
|
|
218
|
-
const gid = Number(status.Gid?.split(/\s+/)[0]);
|
|
219
|
-
const cmdline = readFileSync(resolve(processRoot, "cmdline")).toString("utf8").split("\0").filter(Boolean);
|
|
220
|
-
if (
|
|
221
|
-
uid !== validated.config.uid || gid !== validated.config.gid ||
|
|
222
|
-
status.NoNewPrivs !== "1" || !/^0+$/.test(status.CapEff ?? "") ||
|
|
223
|
-
cmdline.length !== 2 || cmdline[0] !== "/usr/bin/node" || cmdline[1] !== executablePath
|
|
224
|
-
) throw new Error("proxy process identity rejected");
|
|
225
|
-
const nodeExecutable = readlinkSync(resolve(processRoot, "exe"));
|
|
226
|
-
if (!isAbsolute(nodeExecutable)) throw new Error("proxy executable rejected");
|
|
227
|
-
return {
|
|
228
|
-
ok: true,
|
|
229
|
-
pid,
|
|
230
|
-
uid,
|
|
231
|
-
gid,
|
|
232
|
-
host: validated.config.listenHost,
|
|
233
|
-
port: validated.config.listenPort,
|
|
234
|
-
executableDigest: sha256(readFileSync(executablePath)),
|
|
235
|
-
configDigest: sha256(configBytes),
|
|
236
|
-
allowedHosts: [...validated.config.allowedHosts],
|
|
237
|
-
};
|
|
238
|
-
} catch {
|
|
239
|
-
return { ok: false, code: "egress_proxy_observation_rejected" };
|
|
240
|
-
}
|
|
241
|
-
}
|