@tealbrick/avm 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.
- package/LICENSE +21 -0
- package/PROVENANCE.md +38 -0
- package/README.md +165 -0
- package/dist/_chunks/extension-CS1otfhf.mjs +232 -0
- package/dist/_chunks/src-BhUiVsTW.mjs +522 -0
- package/dist/client/administration.d.ts +3 -0
- package/dist/client/administration.js +145 -0
- package/dist/client/config.d.ts +1125 -0
- package/dist/client/config.js +257 -0
- package/dist/client/index.d.ts +126 -0
- package/dist/client/index.js +364 -0
- package/dist/extension/_manifest.json +11 -0
- package/dist/extension/extension.d.ts +626 -0
- package/dist/extension/extension.mjs +8 -0
- package/dist/extension/tools/action.d.ts +24 -0
- package/dist/extension/tools/action.mjs +46 -0
- package/dist/extension/tools/inspect.d.ts +50 -0
- package/dist/extension/tools/inspect.mjs +66 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +8 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.mjs +9 -0
- package/package.json +70 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { administrationArgv } from "./administration.js";
|
|
2
|
+
export { avmConfigSchema, connectionSchema, policySchema, administrationSchema, } from "./config.js";
|
|
3
|
+
export const AVMM_COMPATIBILITY = {
|
|
4
|
+
version: "0.13.0-alpha.2",
|
|
5
|
+
revision: "75078743b9bfc2ba268f1fdeea74f298983dfac4",
|
|
6
|
+
};
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
/** Errors intentionally exclude command arguments, remote messages, stdout and stderr. */
|
|
9
|
+
export class AvmError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
exitCode;
|
|
12
|
+
constructor(code, exitCode) {
|
|
13
|
+
super(`AgentVM ${code.toLowerCase().replaceAll("_", " ")}`);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.exitCode = exitCode;
|
|
16
|
+
this.name = "AvmError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const identifier = (value) => {
|
|
20
|
+
if (typeof value !== "string" ||
|
|
21
|
+
!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value))
|
|
22
|
+
throw new AvmError("INVALID_REQUEST");
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
const limit = (value = 50) => {
|
|
26
|
+
if (!Number.isInteger(value) || value < 1 || value > 200)
|
|
27
|
+
throw new AvmError("INVALID_REQUEST");
|
|
28
|
+
return String(value);
|
|
29
|
+
};
|
|
30
|
+
export function commandArgv(request, policy = {}) {
|
|
31
|
+
const prefix = ["--json"];
|
|
32
|
+
switch (request.operation) {
|
|
33
|
+
case "configured": {
|
|
34
|
+
const grant = policy.administration?.find((g) => g.name === identifier(request.action));
|
|
35
|
+
if (!grant)
|
|
36
|
+
throw new AvmError("DENIED");
|
|
37
|
+
try {
|
|
38
|
+
return [...prefix, ...administrationArgv(grant.request)];
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new AvmError("INVALID_REQUEST");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
case "poolStatus":
|
|
45
|
+
case "poolPressure":
|
|
46
|
+
case "poolQueue":
|
|
47
|
+
return [...prefix, "pool", request.operation.slice(4).toLowerCase()];
|
|
48
|
+
case "poolPolicy":
|
|
49
|
+
return [...prefix, "pool", "policy", "show"];
|
|
50
|
+
case "workspaces":
|
|
51
|
+
return [...prefix, "workspace", "ls"];
|
|
52
|
+
case "users":
|
|
53
|
+
return [...prefix, "user", "ls"];
|
|
54
|
+
case "workspace":
|
|
55
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(request.id))
|
|
56
|
+
throw new AvmError("INVALID_REQUEST");
|
|
57
|
+
return [...prefix, "workspace", "show", request.id];
|
|
58
|
+
case "user":
|
|
59
|
+
return [...prefix, "user", "show", identifier(request.id)];
|
|
60
|
+
case "owner":
|
|
61
|
+
return [...prefix, "agent", "owner", "get", identifier(request.worker)];
|
|
62
|
+
case "shares":
|
|
63
|
+
return [...prefix, "agent", "share", "ls", identifier(request.worker)];
|
|
64
|
+
case "resident":
|
|
65
|
+
return [...prefix, "agent", "resident", "get"];
|
|
66
|
+
case "ingress":
|
|
67
|
+
return [
|
|
68
|
+
...prefix,
|
|
69
|
+
"ingress",
|
|
70
|
+
"ls",
|
|
71
|
+
...(request.worker ? ["--agent", identifier(request.worker)] : []),
|
|
72
|
+
];
|
|
73
|
+
case "doctor":
|
|
74
|
+
case "inventory":
|
|
75
|
+
return [...prefix, request.operation];
|
|
76
|
+
case "workers":
|
|
77
|
+
return [...prefix, "worker", "ls"];
|
|
78
|
+
case "bundles":
|
|
79
|
+
return [...prefix, "bundle", "ls"];
|
|
80
|
+
case "monitor":
|
|
81
|
+
return [...prefix, "monitor", "status"];
|
|
82
|
+
case "workerStatus":
|
|
83
|
+
return [...prefix, "worker", "status", identifier(request.worker)];
|
|
84
|
+
case "capabilities":
|
|
85
|
+
return [...prefix, "capability", "manifest", identifier(request.worker)];
|
|
86
|
+
case "runs":
|
|
87
|
+
return [...prefix, "runs", "ls", "--limit", limit(request.limit)];
|
|
88
|
+
case "events":
|
|
89
|
+
return [
|
|
90
|
+
...prefix,
|
|
91
|
+
"events",
|
|
92
|
+
"--limit",
|
|
93
|
+
limit(request.limit),
|
|
94
|
+
...[
|
|
95
|
+
["--vm", request.worker],
|
|
96
|
+
["--event", request.event],
|
|
97
|
+
["--run-id", request.runId],
|
|
98
|
+
].flatMap(([flag, value]) => value === undefined ? [] : [flag, identifier(value)]),
|
|
99
|
+
];
|
|
100
|
+
case "runStatus":
|
|
101
|
+
return [...prefix, "runs", "status", identifier(request.runId)];
|
|
102
|
+
case "start":
|
|
103
|
+
case "stop":
|
|
104
|
+
case "promote":
|
|
105
|
+
case "destroy": {
|
|
106
|
+
const worker = identifier(request.worker);
|
|
107
|
+
if (!policy.lifecycle?.some((g) => g.worker === worker &&
|
|
108
|
+
g.operations.includes(request.operation)))
|
|
109
|
+
throw new AvmError("DENIED");
|
|
110
|
+
return [...prefix, "worker", request.operation, worker];
|
|
111
|
+
}
|
|
112
|
+
case "spawn": {
|
|
113
|
+
const worker = identifier(request.worker);
|
|
114
|
+
const grant = policy.spawns?.find((g) => g.worker === worker);
|
|
115
|
+
if (!grant)
|
|
116
|
+
throw new AvmError("DENIED");
|
|
117
|
+
if (!["light", "small", "standard", "browser"].includes(grant.profile) ||
|
|
118
|
+
!["ephemeral", "durable"].includes(grant.mode))
|
|
119
|
+
throw new AvmError("INVALID_REQUEST");
|
|
120
|
+
const argv = [
|
|
121
|
+
...prefix,
|
|
122
|
+
"worker",
|
|
123
|
+
"create",
|
|
124
|
+
worker,
|
|
125
|
+
"--profile",
|
|
126
|
+
grant.profile,
|
|
127
|
+
"--mode",
|
|
128
|
+
grant.mode,
|
|
129
|
+
];
|
|
130
|
+
for (const [flag, values] of [
|
|
131
|
+
["--harness", grant.harnesses],
|
|
132
|
+
["--mcp", grant.mcps],
|
|
133
|
+
["--plugin", grant.plugins],
|
|
134
|
+
["--bundle", grant.bundles],
|
|
135
|
+
["--cap", grant.capabilities],
|
|
136
|
+
]) {
|
|
137
|
+
for (const value of values ?? []) {
|
|
138
|
+
if (!/^[A-Za-z0-9@][A-Za-z0-9@_./:+-]{0,255}$/.test(value))
|
|
139
|
+
throw new AvmError("INVALID_REQUEST");
|
|
140
|
+
argv.push(flag, value);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const value of grant.auth ?? []) {
|
|
144
|
+
if (!/^[a-z][a-z0-9_-]*=(none|local)$|^codex=shared$/.test(value))
|
|
145
|
+
throw new AvmError("INVALID_REQUEST");
|
|
146
|
+
argv.push("--auth", value);
|
|
147
|
+
}
|
|
148
|
+
return argv;
|
|
149
|
+
}
|
|
150
|
+
case "exec": {
|
|
151
|
+
const worker = identifier(request.worker);
|
|
152
|
+
const grant = policy.commands?.find((g) => g.worker === worker && g.command === request.command);
|
|
153
|
+
if (!grant)
|
|
154
|
+
throw new AvmError("DENIED");
|
|
155
|
+
if (!grant.argv.length ||
|
|
156
|
+
grant.argv.some((a) => typeof a !== "string" || a.includes("\0") || a.length > 8192))
|
|
157
|
+
throw new AvmError("INVALID_REQUEST");
|
|
158
|
+
return [...prefix, "worker", "exec", worker, "--", ...grant.argv];
|
|
159
|
+
}
|
|
160
|
+
default:
|
|
161
|
+
throw new AvmError("INVALID_REQUEST");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export class AvmClient {
|
|
165
|
+
transport;
|
|
166
|
+
policy;
|
|
167
|
+
constructor(transport, options = {}) {
|
|
168
|
+
this.transport = transport;
|
|
169
|
+
this.policy = structuredClone(options.policy ?? {});
|
|
170
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
171
|
+
if (!Number.isInteger(this.timeoutMs) ||
|
|
172
|
+
this.timeoutMs < 1 ||
|
|
173
|
+
this.timeoutMs > 300_000)
|
|
174
|
+
throw new AvmError("INVALID_REQUEST");
|
|
175
|
+
}
|
|
176
|
+
timeoutMs;
|
|
177
|
+
async request(request, options = {}) {
|
|
178
|
+
const argv = commandArgv(request, this.policy);
|
|
179
|
+
if (options.signal?.aborted)
|
|
180
|
+
throw new AvmError("CANCELLED");
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
let rejectAbort = () => { };
|
|
183
|
+
const aborted = new Promise((_, reject) => {
|
|
184
|
+
rejectAbort = reject;
|
|
185
|
+
});
|
|
186
|
+
const abort = (code) => {
|
|
187
|
+
controller.abort();
|
|
188
|
+
rejectAbort(new AvmError(code));
|
|
189
|
+
};
|
|
190
|
+
const onAbort = () => abort("CANCELLED");
|
|
191
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
192
|
+
const timer = setTimeout(() => abort("TIMEOUT"), this.timeoutMs);
|
|
193
|
+
try {
|
|
194
|
+
const raw = await Promise.race([
|
|
195
|
+
this.transport.run(argv, { signal: controller.signal }),
|
|
196
|
+
aborted,
|
|
197
|
+
]);
|
|
198
|
+
let envelope;
|
|
199
|
+
try {
|
|
200
|
+
envelope = JSON.parse(raw.stdout);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
throw new AvmError(raw.exitCode ? "TRANSPORT" : "PROTOCOL", raw.exitCode);
|
|
204
|
+
}
|
|
205
|
+
if (!envelope ||
|
|
206
|
+
typeof envelope !== "object" ||
|
|
207
|
+
Array.isArray(envelope) ||
|
|
208
|
+
envelope.api_version !== "agentvm/v1")
|
|
209
|
+
throw new AvmError("PROTOCOL");
|
|
210
|
+
const response = envelope;
|
|
211
|
+
if ("error" in response)
|
|
212
|
+
throw new AvmError("REMOTE", raw.exitCode);
|
|
213
|
+
if (!("result" in response))
|
|
214
|
+
throw new AvmError("PROTOCOL");
|
|
215
|
+
if (request.operation === "exec") {
|
|
216
|
+
const result = response.result;
|
|
217
|
+
if (!result ||
|
|
218
|
+
typeof result !== "object" ||
|
|
219
|
+
Array.isArray(result) ||
|
|
220
|
+
!Number.isInteger(result.returncode) ||
|
|
221
|
+
typeof result.stdout !== "string" ||
|
|
222
|
+
typeof result.stderr !== "string")
|
|
223
|
+
throw new AvmError("PROTOCOL");
|
|
224
|
+
if (result.returncode !== 0)
|
|
225
|
+
throw new AvmError("GUEST_FAILED", result.returncode);
|
|
226
|
+
}
|
|
227
|
+
if (raw.exitCode !== 0)
|
|
228
|
+
throw new AvmError("TRANSPORT", raw.exitCode);
|
|
229
|
+
return response.result;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error instanceof AvmError)
|
|
233
|
+
throw error;
|
|
234
|
+
throw new AvmError("TRANSPORT");
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Executes host-owned AVM remotely: operator JSON stdin or paired, quoted ForceCommand protocol. */
|
|
243
|
+
export class SshTransport {
|
|
244
|
+
target;
|
|
245
|
+
mode;
|
|
246
|
+
identityFile;
|
|
247
|
+
identityTokenFile;
|
|
248
|
+
command;
|
|
249
|
+
maxOutputBytes;
|
|
250
|
+
constructor(options) {
|
|
251
|
+
if (!/^(?:[A-Za-z0-9_][A-Za-z0-9_.-]*@)?[A-Za-z0-9][A-Za-z0-9.-]*$/.test(options.target))
|
|
252
|
+
throw new AvmError("INVALID_REQUEST");
|
|
253
|
+
this.mode = options.mode ?? "operator";
|
|
254
|
+
if (!["operator", "paired"].includes(this.mode))
|
|
255
|
+
throw new AvmError("INVALID_REQUEST");
|
|
256
|
+
for (const path of [options.identityFile, options.identityTokenFile])
|
|
257
|
+
if (path !== undefined &&
|
|
258
|
+
(!/^\/[A-Za-z0-9_./ -]+$/.test(path) || path.length > 4096))
|
|
259
|
+
throw new AvmError("INVALID_REQUEST");
|
|
260
|
+
if (this.mode === "paired" &&
|
|
261
|
+
(!options.target.startsWith("agentvm-client@") ||
|
|
262
|
+
!options.identityFile ||
|
|
263
|
+
options.sudo ||
|
|
264
|
+
options.identityTokenFile))
|
|
265
|
+
throw new AvmError("INVALID_REQUEST");
|
|
266
|
+
this.identityFile = options.identityFile;
|
|
267
|
+
this.identityTokenFile = options.identityTokenFile;
|
|
268
|
+
this.target = options.target;
|
|
269
|
+
this.maxOutputBytes = options.maxOutputBytes ?? 1_048_576;
|
|
270
|
+
if (!Number.isInteger(this.maxOutputBytes) ||
|
|
271
|
+
this.maxOutputBytes < 1024 ||
|
|
272
|
+
this.maxOutputBytes > 16_777_216)
|
|
273
|
+
throw new AvmError("INVALID_REQUEST");
|
|
274
|
+
// This entire remote command is constant; no caller values enter it.
|
|
275
|
+
this.command = options.sudo
|
|
276
|
+
? `/usr/bin/python3 -c 'import json,subprocess,sys;sys.exit(subprocess.call(["sudo","-n","/usr/local/bin/avm"]+json.load(sys.stdin)))'`
|
|
277
|
+
: `/usr/bin/python3 -c 'import json,subprocess,sys;sys.exit(subprocess.call(["/usr/local/bin/avm"]+json.load(sys.stdin)))'`;
|
|
278
|
+
}
|
|
279
|
+
run(argv, { signal }) {
|
|
280
|
+
if (signal.aborted)
|
|
281
|
+
return Promise.reject(new AvmError("CANCELLED"));
|
|
282
|
+
if (argv.some((a) => typeof a !== "string" || a.includes("\0") || a.length > 8192))
|
|
283
|
+
return Promise.reject(new AvmError("INVALID_REQUEST"));
|
|
284
|
+
const remoteArgv = this.identityTokenFile
|
|
285
|
+
? ["--identity-token-file", this.identityTokenFile, ...argv]
|
|
286
|
+
: [...argv];
|
|
287
|
+
const quote = (value) => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
288
|
+
const command = this.mode === "paired"
|
|
289
|
+
? "exec /usr/local/bin/avm " + remoteArgv.map(quote).join(" ")
|
|
290
|
+
: this.command;
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const child = spawn("/usr/bin/ssh", [
|
|
293
|
+
...(this.mode === "paired" ? ["-F", "/dev/null"] : []),
|
|
294
|
+
"-T",
|
|
295
|
+
"-o",
|
|
296
|
+
"ForwardAgent=no",
|
|
297
|
+
"-o",
|
|
298
|
+
"ClearAllForwardings=yes",
|
|
299
|
+
"-o",
|
|
300
|
+
"PermitLocalCommand=no",
|
|
301
|
+
"-o",
|
|
302
|
+
"BatchMode=yes",
|
|
303
|
+
"-o",
|
|
304
|
+
"ConnectTimeout=10",
|
|
305
|
+
"-o",
|
|
306
|
+
"StrictHostKeyChecking=yes",
|
|
307
|
+
...(this.identityFile
|
|
308
|
+
? [
|
|
309
|
+
"-o",
|
|
310
|
+
"IdentitiesOnly=yes",
|
|
311
|
+
"-o",
|
|
312
|
+
"IdentityAgent=none",
|
|
313
|
+
"-i",
|
|
314
|
+
this.identityFile,
|
|
315
|
+
]
|
|
316
|
+
: []),
|
|
317
|
+
"--",
|
|
318
|
+
this.target,
|
|
319
|
+
command,
|
|
320
|
+
], {
|
|
321
|
+
shell: false,
|
|
322
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
323
|
+
env: { PATH: "/usr/bin:/bin", HOME: process.env.HOME },
|
|
324
|
+
});
|
|
325
|
+
const chunks = [];
|
|
326
|
+
let bytes = 0;
|
|
327
|
+
let failure;
|
|
328
|
+
const stop = () => {
|
|
329
|
+
failure ??= new AvmError("CANCELLED");
|
|
330
|
+
child.kill("SIGKILL");
|
|
331
|
+
};
|
|
332
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
333
|
+
const collect = (data, keep) => {
|
|
334
|
+
bytes += data.length;
|
|
335
|
+
if (bytes > this.maxOutputBytes) {
|
|
336
|
+
failure = new AvmError("OUTPUT_LIMIT");
|
|
337
|
+
child.kill("SIGKILL");
|
|
338
|
+
}
|
|
339
|
+
else if (keep)
|
|
340
|
+
chunks.push(data);
|
|
341
|
+
};
|
|
342
|
+
child.stdout.on("data", (data) => collect(data, true));
|
|
343
|
+
child.stderr.on("data", (data) => collect(data, false));
|
|
344
|
+
child.stdin.on("error", () => { });
|
|
345
|
+
child.on("error", () => {
|
|
346
|
+
signal.removeEventListener("abort", stop);
|
|
347
|
+
reject(new AvmError("TRANSPORT"));
|
|
348
|
+
});
|
|
349
|
+
child.on("close", (code) => {
|
|
350
|
+
signal.removeEventListener("abort", stop);
|
|
351
|
+
if (failure)
|
|
352
|
+
reject(failure);
|
|
353
|
+
else
|
|
354
|
+
resolve({
|
|
355
|
+
stdout: Buffer.concat(chunks).toString("utf8"),
|
|
356
|
+
exitCode: code ?? 255,
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
child.stdin.end(this.mode === "paired" ? undefined : JSON.stringify(remoteArgv));
|
|
360
|
+
if (signal.aborted)
|
|
361
|
+
stop();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|