@mifune/agro 0.20.0 → 0.22.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/README.md +3 -3
- package/dist/agro.js +716 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The **AGRO CLI** (`agro`) — create an [AGRO](https://github.com/mifunedev/agro
|
|
|
4
4
|
Docker sandbox for coding agents and drive its lifecycle from the command line.
|
|
5
5
|
|
|
6
6
|
AGRO is a portable harness for running coding agents (Claude Code, Codex, Pi,
|
|
7
|
-
and others) in an isolated Docker sandbox, with the agent's identity
|
|
7
|
+
and others) in an isolated Docker sandbox, with the agent's identity and skills
|
|
8
8
|
versioned in git. This package is the standalone `agro` CLI that creates sandboxes and
|
|
9
9
|
manages their lifecycle.
|
|
10
10
|
|
|
@@ -54,7 +54,7 @@ To bind one of your own checkouts into the sandbox, equip it and pass `--checkou
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
cd your-project
|
|
57
|
-
agro vendor # vendor .agro/
|
|
57
|
+
agro vendor # vendor .agro/ — and nothing else (compatibility window)
|
|
58
58
|
agro sandbox install docker --checkout "$PWD" --name your-project
|
|
59
59
|
```
|
|
60
60
|
|
|
@@ -133,7 +133,7 @@ conflicting `AGRO_SANDBOX_IMAGE` values in the host shell or entry `.env` before
|
|
|
133
133
|
|
|
134
134
|
`agro update` upgrades only the installed CLI. It does not recreate the sandbox
|
|
135
135
|
or change its image. During the compatibility window `agro vendor` vendors
|
|
136
|
-
`.agro/`
|
|
136
|
+
`.agro/` into the current directory, equipping an empty checkout and upgrading an
|
|
137
137
|
equipped one (`--from <dir>` / `--from-remote [--ref <ref>]`, `--dry-run`, `--force`); it
|
|
138
138
|
writes nothing else and never prompts. `agro update` rejects those flags and points at
|
|
139
139
|
`agro vendor`. `agro vendor` prefers the payload bundled into the CLI itself; with `--from-remote`
|
package/dist/agro.js
CHANGED
|
@@ -908,6 +908,7 @@ var RESERVED_HOME_PATHS = [
|
|
|
908
908
|
"/usr",
|
|
909
909
|
"/var"
|
|
910
910
|
];
|
|
911
|
+
var EGRESS_MODES = ["open", "allowlist"];
|
|
911
912
|
var SANDBOX_RUNTIMES = PROVISIONABLE_RUNTIMES;
|
|
912
913
|
function configCheckout(config) {
|
|
913
914
|
const value = config.checkout ?? config.repo;
|
|
@@ -927,11 +928,11 @@ function defaultAgroConfig(name) {
|
|
|
927
928
|
ssh: false,
|
|
928
929
|
sshPort: 2222,
|
|
929
930
|
sshPasswordAuth: false,
|
|
930
|
-
dockerSocket: false
|
|
931
|
+
dockerSocket: false,
|
|
932
|
+
egress: "open",
|
|
933
|
+
egressAllow: []
|
|
931
934
|
},
|
|
932
935
|
hermesDashboard: { enabled: false, port: 9119 },
|
|
933
|
-
cron: { agentBin: "claude" },
|
|
934
|
-
build: { skipPnpmInstall: false },
|
|
935
936
|
image: { mode: "build", pullPolicy: "missing" },
|
|
936
937
|
langfuse: {},
|
|
937
938
|
composeOverrides: []
|
|
@@ -1019,16 +1020,14 @@ function validateAgroConfig(value) {
|
|
|
1019
1020
|
expectBoolean(access, "sshPasswordAuth", "access.");
|
|
1020
1021
|
expectString(access, "sshAuthorizedKeys", "access.");
|
|
1021
1022
|
expectBoolean(access, "dockerSocket", "access.");
|
|
1023
|
+
expectEnum(access, "egress", "access.", EGRESS_MODES);
|
|
1024
|
+
expectStringArray(access, "egressAllow", "access.");
|
|
1022
1025
|
}
|
|
1023
1026
|
const dashboard = expectSection(record, "hermesDashboard");
|
|
1024
1027
|
if (dashboard) {
|
|
1025
1028
|
expectBoolean(dashboard, "enabled", "hermesDashboard.");
|
|
1026
1029
|
expectPort(dashboard, "port", "hermesDashboard.");
|
|
1027
1030
|
}
|
|
1028
|
-
const cron = expectSection(record, "cron");
|
|
1029
|
-
if (cron) expectString(cron, "agentBin", "cron.");
|
|
1030
|
-
const build = expectSection(record, "build");
|
|
1031
|
-
if (build) expectBoolean(build, "skipPnpmInstall", "build.");
|
|
1032
1031
|
const image = expectSection(record, "image");
|
|
1033
1032
|
if (image) {
|
|
1034
1033
|
expectString(image, "ref", "image.");
|
|
@@ -1086,6 +1085,16 @@ function expectPort(record, key, prefix = "") {
|
|
|
1086
1085
|
throw fieldError(`${prefix}${key}`, "must be an integer between 1 and 65535");
|
|
1087
1086
|
}
|
|
1088
1087
|
}
|
|
1088
|
+
function expectStringArray(record, key, prefix = "") {
|
|
1089
|
+
const value = record[key];
|
|
1090
|
+
if (value === void 0) return;
|
|
1091
|
+
if (!Array.isArray(value)) throw fieldError(`${prefix}${key}`, "must be an array of strings");
|
|
1092
|
+
for (const entry of value) {
|
|
1093
|
+
if (typeof entry !== "string") {
|
|
1094
|
+
throw fieldError(`${prefix}${key}`, "must be an array of strings");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1089
1098
|
function expectEnum(record, key, prefix, allowed) {
|
|
1090
1099
|
const value = record[key];
|
|
1091
1100
|
if (value === void 0) return;
|
|
@@ -1106,10 +1115,10 @@ var AGRO_CONFIG_FIELDS = [
|
|
|
1106
1115
|
{ path: "access.sshPasswordAuth", type: "boolean" },
|
|
1107
1116
|
{ path: "access.sshAuthorizedKeys", type: "string" },
|
|
1108
1117
|
{ path: "access.dockerSocket", type: "boolean" },
|
|
1118
|
+
{ path: "access.egress", type: "enum", values: EGRESS_MODES },
|
|
1119
|
+
{ path: "access.egressAllow", type: "list" },
|
|
1109
1120
|
{ path: "hermesDashboard.enabled", type: "boolean" },
|
|
1110
1121
|
{ path: "hermesDashboard.port", type: "port" },
|
|
1111
|
-
{ path: "cron.agentBin", type: "string" },
|
|
1112
|
-
{ path: "build.skipPnpmInstall", type: "boolean" },
|
|
1113
1122
|
{ path: "image.ref", type: "string" },
|
|
1114
1123
|
{ path: "image.mode", type: "enum", values: ["build", "image"] },
|
|
1115
1124
|
{ path: "image.pullPolicy", type: "enum", values: ["missing", "always", "never"] },
|
|
@@ -1415,10 +1424,16 @@ async function askSecretPiped(question) {
|
|
|
1415
1424
|
process.on("SIGINT", onSigint);
|
|
1416
1425
|
try {
|
|
1417
1426
|
return await new Promise((resolve14) => {
|
|
1418
|
-
|
|
1427
|
+
let settled = false;
|
|
1428
|
+
const settle = (answer) => {
|
|
1429
|
+
if (settled) return;
|
|
1430
|
+
settled = true;
|
|
1419
1431
|
process.stdout.write("\n");
|
|
1420
1432
|
resolve14(answer.trim());
|
|
1421
|
-
}
|
|
1433
|
+
};
|
|
1434
|
+
rl.question(` ${question} `, settle);
|
|
1435
|
+
rl.once("line", settle);
|
|
1436
|
+
rl.once("close", () => settle(rl.line));
|
|
1422
1437
|
});
|
|
1423
1438
|
} finally {
|
|
1424
1439
|
process.removeListener("SIGINT", onSigint);
|
|
@@ -1462,10 +1477,10 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as read
|
|
|
1462
1477
|
import { dirname as dirname4, join as join4, resolve as resolve5, sep as sep2 } from "node:path";
|
|
1463
1478
|
|
|
1464
1479
|
// agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.yml
|
|
1465
|
-
var docker_compose_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-sandbox-${SANDBOX_NAME:-agro}}\n pull_policy: ${AGRO_PULL_POLICY:-missing}\n container_name: ${SANDBOX_NAME:-agro}\n build:\n context: ${AGRO_REPO_DIR:-..}\n dockerfile: .devcontainer/Dockerfile\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n - ${AGRO_REPO_DIR:-..}:/home/sandbox/harness\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-
|
|
1480
|
+
var docker_compose_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-sandbox-${SANDBOX_NAME:-agro}}\n pull_policy: ${AGRO_PULL_POLICY:-missing}\n container_name: ${SANDBOX_NAME:-agro}\n build:\n context: ${AGRO_REPO_DIR:-..}\n dockerfile: .devcontainer/Dockerfile\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n - ${AGRO_REPO_DIR:-..}:/home/sandbox/harness\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n - TYPESAFE_API_KEY=${TYPESAFE_API_KEY:-}\n - XAI_API_KEY=${XAI_API_KEY:-}\n stdin_open: true\n tty: true\n init: true\n entrypoint: ["/usr/local/bin/sandbox-main.sh"]\n stop_signal: SIGTERM\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
|
|
1466
1481
|
|
|
1467
1482
|
// agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.image-only.yml
|
|
1468
|
-
var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-ghcr.io/mifunedev/agro:latest}\n pull_policy: ${AGRO_PULL_POLICY:-always}\n container_name: ${SANDBOX_NAME:-agro}\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-
|
|
1483
|
+
var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-ghcr.io/mifunedev/agro:latest}\n pull_policy: ${AGRO_PULL_POLICY:-always}\n container_name: ${SANDBOX_NAME:-agro}\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n - TYPESAFE_API_KEY=${TYPESAFE_API_KEY:-}\n stdin_open: true\n tty: true\n init: true\n entrypoint: ["/usr/local/bin/sandbox-main.sh"]\n stop_signal: SIGTERM\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
|
|
1469
1484
|
|
|
1470
1485
|
// agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.ssh.yml
|
|
1471
1486
|
var docker_compose_ssh_default = 'services:\n sandbox:\n ports:\n - "127.0.0.1:${SANDBOX_SSH_PORT:-2222}:22"\n';
|
|
@@ -1473,6 +1488,9 @@ var docker_compose_ssh_default = 'services:\n sandbox:\n ports:\n - "12
|
|
|
1473
1488
|
// agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.docker-sock.yml
|
|
1474
1489
|
var docker_compose_docker_sock_default = "services:\n sandbox:\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n";
|
|
1475
1490
|
|
|
1491
|
+
// agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.egress.yml
|
|
1492
|
+
var docker_compose_egress_default = 'services:\n sandbox:\n networks:\n - egress-internal\n depends_on:\n - proxy\n environment:\n - HTTPS_PROXY=http://proxy:3128\n - HTTP_PROXY=http://proxy:3128\n - https_proxy=http://proxy:3128\n - http_proxy=http://proxy:3128\n - NO_PROXY=localhost,127.0.0.1\n - no_proxy=localhost,127.0.0.1\n - NODE_USE_ENV_PROXY=1\n\n proxy:\n image: ubuntu/squid@sha256:6a097f68bae708cedbabd6188d68c7e2e7a38cedd05a176e1cc0ba29e3bbe029\n networks:\n - egress-internal\n - egress-out\n configs:\n - source: egress-squid\n target: /etc/squid/squid.conf\n volumes:\n - type: bind\n source: ../egress-allow.txt\n target: /etc/squid/egress-allow.txt\n read_only: true\n restart: unless-stopped\n\nconfigs:\n egress-squid:\n content: |\n http_port 3128\n acl SSL_ports port 443\n acl Safe_ports port 80\n acl Safe_ports port 443\n acl CONNECT method CONNECT\n acl egress_allow dstdomain "/etc/squid/egress-allow.txt"\n http_access deny !Safe_ports\n http_access deny CONNECT !SSL_ports\n http_access allow egress_allow\n http_access deny all\n\nnetworks:\n egress-internal:\n internal: true\n egress-out: {}\n';
|
|
1493
|
+
|
|
1476
1494
|
// agro-asset:/home/runner/work/agro/agro/.agro/scripts/docker-compose.sh
|
|
1477
1495
|
var docker_compose_default2 = `#!/usr/bin/env bash
|
|
1478
1496
|
|
|
@@ -1612,6 +1630,11 @@ fi
|
|
|
1612
1630
|
|
|
1613
1631
|
args+=(-f "$(compose_path ".devcontainer/docker-compose.yml")")
|
|
1614
1632
|
|
|
1633
|
+
egress_value=\${AGRO_EGRESS:-$(read_env_value AGRO_EGRESS)}
|
|
1634
|
+
if [ "$(printf '%s' "$egress_value" | tr '[:upper:]' '[:lower:]')" = "allowlist" ]; then
|
|
1635
|
+
args+=(-f "$(compose_path ".devcontainer/docker-compose.egress.yml")")
|
|
1636
|
+
fi
|
|
1637
|
+
|
|
1615
1638
|
docker_socket_value=\${DOCKER_SOCKET:-$(read_env_value DOCKER_SOCKET)}
|
|
1616
1639
|
if truthy "$docker_socket_value"; then
|
|
1617
1640
|
args+=(-f "$(compose_path ".devcontainer/docker-compose.docker-sock.yml")")
|
|
@@ -1864,9 +1887,29 @@ function nextDefaultName(run = spawnRunner) {
|
|
|
1864
1887
|
function openshellFiles(entry) {
|
|
1865
1888
|
return [{ dest: join4(entry, "openshell-policy.yaml"), body: openshell_policy_default, mode: 420 }];
|
|
1866
1889
|
}
|
|
1867
|
-
function
|
|
1890
|
+
function policyHosts(policyText) {
|
|
1891
|
+
const marker = "network_policies:";
|
|
1892
|
+
const start = policyText.indexOf(marker);
|
|
1893
|
+
const section = start === -1 ? "" : policyText.slice(start);
|
|
1894
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
1895
|
+
for (const match of section.matchAll(/^\s*-\s*host:\s*(\S+)/gm)) hosts.add(match[1]);
|
|
1896
|
+
return [...hosts];
|
|
1897
|
+
}
|
|
1898
|
+
function egressAllowHosts(config) {
|
|
1899
|
+
const hosts = new Set(policyHosts(openshell_policy_default));
|
|
1900
|
+
for (const host of config.access?.egressAllow ?? []) hosts.add(host);
|
|
1901
|
+
return [...hosts].sort();
|
|
1902
|
+
}
|
|
1903
|
+
function safeReadAgroConfig(entry) {
|
|
1904
|
+
try {
|
|
1905
|
+
return readAgroConfig(agroConfigPath(entry));
|
|
1906
|
+
} catch {
|
|
1907
|
+
return { version: 1 };
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
function dockerFiles(entry, opts, config) {
|
|
1868
1911
|
const scripts = join4(resolveProjectLayout(entry).controlDir, "scripts");
|
|
1869
|
-
|
|
1912
|
+
const files = [
|
|
1870
1913
|
{
|
|
1871
1914
|
dest: join4(entry, ".devcontainer", "docker-compose.yml"),
|
|
1872
1915
|
body: (opts.checkout ?? opts.repo) === void 0 ? docker_compose_image_only_default : docker_compose_default,
|
|
@@ -1874,14 +1917,25 @@ function dockerFiles(entry, opts) {
|
|
|
1874
1917
|
},
|
|
1875
1918
|
{ dest: join4(entry, ".devcontainer", "docker-compose.ssh.yml"), body: docker_compose_ssh_default, mode: 420 },
|
|
1876
1919
|
{ dest: join4(entry, ".devcontainer", "docker-compose.docker-sock.yml"), body: docker_compose_docker_sock_default, mode: 420 },
|
|
1920
|
+
{ dest: join4(entry, ".devcontainer", "docker-compose.egress.yml"), body: docker_compose_egress_default, mode: 420 },
|
|
1877
1921
|
{ dest: join4(scripts, "docker-compose.sh"), body: docker_compose_default2, mode: 493 },
|
|
1878
1922
|
{ dest: join4(scripts, "paths.sh"), body: paths_default, mode: 420 },
|
|
1879
1923
|
{ dest: join4(scripts, "check-host-port.sh"), body: check_host_port_default, mode: 493 }
|
|
1880
1924
|
];
|
|
1925
|
+
if ((config.access?.egress ?? "open") === "allowlist") {
|
|
1926
|
+
files.push({
|
|
1927
|
+
dest: join4(entry, "egress-allow.txt"),
|
|
1928
|
+
body: `${egressAllowHosts(config).join("\n")}
|
|
1929
|
+
`,
|
|
1930
|
+
mode: 420
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
return files;
|
|
1881
1934
|
}
|
|
1882
1935
|
function materialize(root, opts = {}) {
|
|
1883
1936
|
const entry = resolve5(root);
|
|
1884
|
-
const
|
|
1937
|
+
const config = safeReadAgroConfig(entry);
|
|
1938
|
+
const files = entryRuntime(entry) === "openshell" ? openshellFiles(entry) : dockerFiles(entry, opts, config);
|
|
1885
1939
|
for (const file of files) {
|
|
1886
1940
|
const dest = resolve5(file.dest);
|
|
1887
1941
|
if (!dest.startsWith(entry + sep2)) {
|
|
@@ -1921,6 +1975,7 @@ function resolveSandboxRoot(opts = {}) {
|
|
|
1921
1975
|
}
|
|
1922
1976
|
|
|
1923
1977
|
// src/lib/secrets.ts
|
|
1978
|
+
import { randomBytes } from "node:crypto";
|
|
1924
1979
|
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
1925
1980
|
import { resolve as resolve6 } from "node:path";
|
|
1926
1981
|
|
|
@@ -1985,7 +2040,15 @@ var SECRET_KEYS = [
|
|
|
1985
2040
|
"PI_SLACK_BOT_TOKEN",
|
|
1986
2041
|
"LANGFUSE_PUBLIC_KEY",
|
|
1987
2042
|
"LANGFUSE_SECRET_KEY",
|
|
1988
|
-
"TYPESAFE_API_KEY"
|
|
2043
|
+
"TYPESAFE_API_KEY",
|
|
2044
|
+
"AGRO_CONSOLE_URL",
|
|
2045
|
+
"AGRO_CONSOLE_TOKEN"
|
|
2046
|
+
];
|
|
2047
|
+
var COMPOSE_SECRET_KEYS = [
|
|
2048
|
+
"GH_TOKEN",
|
|
2049
|
+
"SANDBOX_PASSWORD",
|
|
2050
|
+
"TYPESAFE_API_KEY",
|
|
2051
|
+
"XAI_API_KEY"
|
|
1989
2052
|
];
|
|
1990
2053
|
var ALLOWED = new Set(SECRET_KEYS);
|
|
1991
2054
|
function secretsFilePath(root) {
|
|
@@ -2020,6 +2083,10 @@ function setSecret(root, key, value) {
|
|
|
2020
2083
|
const { content } = setKeyInEnv(original, key, value);
|
|
2021
2084
|
writeEnvFile(file, content);
|
|
2022
2085
|
}
|
|
2086
|
+
function ensureSandboxPassword(root) {
|
|
2087
|
+
if (readSecret(root, "SANDBOX_PASSWORD") !== void 0) return;
|
|
2088
|
+
setSecret(root, "SANDBOX_PASSWORD", randomBytes(24).toString("base64url"));
|
|
2089
|
+
}
|
|
2023
2090
|
function listSecretKeys(root) {
|
|
2024
2091
|
const file = secretsFilePath(root);
|
|
2025
2092
|
if (!existsSync8(file)) return [];
|
|
@@ -2046,7 +2113,7 @@ import { basename as basename6, join as join6, sep as sep3 } from "node:path";
|
|
|
2046
2113
|
import { basename as basename4, resolve as resolve7 } from "node:path";
|
|
2047
2114
|
|
|
2048
2115
|
// src/lib/version.ts
|
|
2049
|
-
var AGRO_VERSION = true ? "0.
|
|
2116
|
+
var AGRO_VERSION = true ? "0.22.0" : "0.0.0-dev";
|
|
2050
2117
|
var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
|
|
2051
2118
|
var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
|
|
2052
2119
|
function parseReleaseVersion(value) {
|
|
@@ -2621,6 +2688,7 @@ function renderComposeVars(config) {
|
|
|
2621
2688
|
put("GIT_USER_NAME", config.git?.userName);
|
|
2622
2689
|
put("GIT_USER_EMAIL", config.git?.userEmail);
|
|
2623
2690
|
put("DOCKER_SOCKET", config.access?.dockerSocket);
|
|
2691
|
+
put("AGRO_EGRESS", config.access?.egress);
|
|
2624
2692
|
put("SANDBOX_SSH", config.access?.ssh);
|
|
2625
2693
|
put("SANDBOX_SSH_PORT", config.access?.sshPort);
|
|
2626
2694
|
put("AGRO_SANDBOX_IMAGE", sandboxImageRef(config));
|
|
@@ -2848,6 +2916,7 @@ async function runSandbox(opts, io) {
|
|
|
2848
2916
|
}
|
|
2849
2917
|
await maybePromptDockerSocket(root, io, opts.bin);
|
|
2850
2918
|
requireLifecycleScript(root, "docker-compose.sh");
|
|
2919
|
+
ensureSandboxPassword(root);
|
|
2851
2920
|
if (imageRef !== void 0) {
|
|
2852
2921
|
io.stdout(`image mode: ${imageRef} (skipping local build)
|
|
2853
2922
|
`);
|
|
@@ -3368,6 +3437,14 @@ Set it with \`${opts.bin} config set ${key}\` instead.
|
|
|
3368
3437
|
|
|
3369
3438
|
Keys:
|
|
3370
3439
|
${secretKeyList()}
|
|
3440
|
+
`
|
|
3441
|
+
);
|
|
3442
|
+
return 1;
|
|
3443
|
+
}
|
|
3444
|
+
if (opts.sandbox !== void 0 && !COMPOSE_SECRET_KEYS.includes(key)) {
|
|
3445
|
+
io.stderr(
|
|
3446
|
+
`${opts.bin} secret set: ${key} does not reach the container from --sandbox ${opts.sandbox} \u2014 Compose passes only ${COMPOSE_SECRET_KEYS.join(", ")}.
|
|
3447
|
+
Run \`${opts.bin} secret set ${key}\` in the project checkout instead.
|
|
3371
3448
|
`
|
|
3372
3449
|
);
|
|
3373
3450
|
return 1;
|
|
@@ -3388,7 +3465,26 @@ ${secretKeyList()}
|
|
|
3388
3465
|
`);
|
|
3389
3466
|
return 1;
|
|
3390
3467
|
}
|
|
3391
|
-
io.stdout(`.env: set ${key}
|
|
3468
|
+
io.stdout(`.env: set ${key}
|
|
3469
|
+
`);
|
|
3470
|
+
return 0;
|
|
3471
|
+
}
|
|
3472
|
+
async function runSecretGet(key, opts, io) {
|
|
3473
|
+
const root = secretsRoot(opts);
|
|
3474
|
+
let value;
|
|
3475
|
+
try {
|
|
3476
|
+
value = readSecret(root, key);
|
|
3477
|
+
} catch (error) {
|
|
3478
|
+
io.stderr(`${error instanceof Error ? error.message : String(error)}
|
|
3479
|
+
`);
|
|
3480
|
+
return 1;
|
|
3481
|
+
}
|
|
3482
|
+
if (value === void 0) {
|
|
3483
|
+
io.stderr(`${opts.bin} secret get: ${key} has no value
|
|
3484
|
+
`);
|
|
3485
|
+
return 1;
|
|
3486
|
+
}
|
|
3487
|
+
io.stdout(`${value}
|
|
3392
3488
|
`);
|
|
3393
3489
|
return 0;
|
|
3394
3490
|
}
|
|
@@ -3403,7 +3499,7 @@ async function runSecretList(opts, io) {
|
|
|
3403
3499
|
const width = Math.max(...keys.map((key) => key.length));
|
|
3404
3500
|
for (const key of keys) {
|
|
3405
3501
|
const value = readSecret(root, key);
|
|
3406
|
-
io.stdout(`${key.padEnd(width)} ${value === void 0 ? "\u2014" :
|
|
3502
|
+
io.stdout(`${key.padEnd(width)} ${value === void 0 ? "\u2014" : "set"}
|
|
3407
3503
|
`);
|
|
3408
3504
|
}
|
|
3409
3505
|
return 0;
|
|
@@ -3415,6 +3511,9 @@ import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
|
|
|
3415
3511
|
import { dirname as dirname7, join as join11, relative } from "node:path";
|
|
3416
3512
|
|
|
3417
3513
|
// src/lib/tracing/harness-writers.ts
|
|
3514
|
+
import { join as join10 } from "node:path";
|
|
3515
|
+
|
|
3516
|
+
// src/lib/tracing/providers/langfuse.ts
|
|
3418
3517
|
import { join as join9 } from "node:path";
|
|
3419
3518
|
|
|
3420
3519
|
// src/lib/tracing/writer.ts
|
|
@@ -3457,16 +3556,66 @@ function isPlainObject(value) {
|
|
|
3457
3556
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3458
3557
|
}
|
|
3459
3558
|
|
|
3559
|
+
// src/lib/tracing/providers/langfuse.ts
|
|
3560
|
+
var LANGFUSE_PLUGIN_MAX_CHARS = 1e5;
|
|
3561
|
+
var LANGFUSE_FRAGMENT_KEYS = [
|
|
3562
|
+
"LANGFUSE_PUBLIC_KEY",
|
|
3563
|
+
"LANGFUSE_SECRET_KEY",
|
|
3564
|
+
"LANGFUSE_BASE_URL",
|
|
3565
|
+
"LANGFUSE_TRACING_ENVIRONMENT",
|
|
3566
|
+
"PI_LANGFUSE_MAX_CHARS"
|
|
3567
|
+
];
|
|
3568
|
+
function langfuseFragmentPath(home) {
|
|
3569
|
+
return join9(home, ".config", "agro", "langfuse.env");
|
|
3570
|
+
}
|
|
3571
|
+
function loadLangfuseCredentials(root) {
|
|
3572
|
+
const publicKey = readSecret(root, "LANGFUSE_PUBLIC_KEY");
|
|
3573
|
+
const secretKey = readSecret(root, "LANGFUSE_SECRET_KEY");
|
|
3574
|
+
if (publicKey !== void 0 && secretKey !== void 0) return { publicKey, secretKey };
|
|
3575
|
+
const missing = [
|
|
3576
|
+
publicKey === void 0 ? "LANGFUSE_PUBLIC_KEY" : void 0,
|
|
3577
|
+
secretKey === void 0 ? "LANGFUSE_SECRET_KEY" : void 0
|
|
3578
|
+
].filter((key) => key !== void 0);
|
|
3579
|
+
throw new Error(
|
|
3580
|
+
`${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not set in .env \u2014 run \`${activeBin()} secret set ${missing[0]} <value>\` first`
|
|
3581
|
+
);
|
|
3582
|
+
}
|
|
3583
|
+
var BARE_SAFE_VALUE = /^[A-Za-z0-9_.:/@%+=,-]+$/;
|
|
3584
|
+
var UNCARRIABLE_IN_VALUE = /[\u0000-\u001f\u007f']/;
|
|
3585
|
+
function renderLangfuseFragment(input) {
|
|
3586
|
+
const values = {
|
|
3587
|
+
LANGFUSE_PUBLIC_KEY: input.publicKey,
|
|
3588
|
+
LANGFUSE_SECRET_KEY: input.secretKey,
|
|
3589
|
+
LANGFUSE_BASE_URL: input.baseUrl,
|
|
3590
|
+
LANGFUSE_TRACING_ENVIRONMENT: input.environment,
|
|
3591
|
+
PI_LANGFUSE_MAX_CHARS: String(LANGFUSE_PLUGIN_MAX_CHARS)
|
|
3592
|
+
};
|
|
3593
|
+
return LANGFUSE_FRAGMENT_KEYS.map((key) => `${key}=${quoteFragmentValue(key, values[key])}
|
|
3594
|
+
`).join("");
|
|
3595
|
+
}
|
|
3596
|
+
function writeLangfuseFragment(home, input) {
|
|
3597
|
+
return writeTextIfChanged(langfuseFragmentPath(home), renderLangfuseFragment(input), PRIVATE_FILE_MODE);
|
|
3598
|
+
}
|
|
3599
|
+
function quoteFragmentValue(key, value) {
|
|
3600
|
+
if (value === "") throw new Error(`${key} must not be empty`);
|
|
3601
|
+
if (UNCARRIABLE_IN_VALUE.test(value)) {
|
|
3602
|
+
throw new Error(
|
|
3603
|
+
`${key} must not contain a newline, a control character, or a single quote \u2014 zsh reads the fragment and does not carry that value safely`
|
|
3604
|
+
);
|
|
3605
|
+
}
|
|
3606
|
+
return BARE_SAFE_VALUE.test(value) ? value : `'${value}'`;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3460
3609
|
// src/lib/tracing/harness-writers.ts
|
|
3461
3610
|
var CODEX_TRACING_TAGS = ["codex"];
|
|
3462
3611
|
function claudeCodeSettingsPath(home) {
|
|
3463
|
-
return
|
|
3612
|
+
return join10(home, ".claude", "settings.json");
|
|
3464
3613
|
}
|
|
3465
3614
|
function piTracingConfigPath(home) {
|
|
3466
|
-
return
|
|
3615
|
+
return join10(home, ".pi", "agent", "langfuse.json");
|
|
3467
3616
|
}
|
|
3468
3617
|
function codexTracingConfigPath(home) {
|
|
3469
|
-
return
|
|
3618
|
+
return join10(home, ".codex", "langfuse.json");
|
|
3470
3619
|
}
|
|
3471
3620
|
var claudeCodeTracingWriter = (home, settings) => {
|
|
3472
3621
|
const path4 = claudeCodeSettingsPath(home);
|
|
@@ -3480,7 +3629,8 @@ var claudeCodeTracingWriter = (home, settings) => {
|
|
|
3480
3629
|
env: {
|
|
3481
3630
|
...env,
|
|
3482
3631
|
LANGFUSE_BASE_URL: settings.baseUrl,
|
|
3483
|
-
LANGFUSE_TRACING_ENVIRONMENT: settings.environment
|
|
3632
|
+
LANGFUSE_TRACING_ENVIRONMENT: settings.environment,
|
|
3633
|
+
CC_LANGFUSE_MAX_CHARS: String(LANGFUSE_PLUGIN_MAX_CHARS)
|
|
3484
3634
|
}
|
|
3485
3635
|
};
|
|
3486
3636
|
return writeTextIfChanged(path4, renderJson(next));
|
|
@@ -3709,54 +3859,6 @@ function harnessBinPath(prefix) {
|
|
|
3709
3859
|
return `${prefix}/bin`;
|
|
3710
3860
|
}
|
|
3711
3861
|
|
|
3712
|
-
// src/lib/tracing/providers/langfuse.ts
|
|
3713
|
-
import { join as join10 } from "node:path";
|
|
3714
|
-
var LANGFUSE_FRAGMENT_KEYS = [
|
|
3715
|
-
"LANGFUSE_PUBLIC_KEY",
|
|
3716
|
-
"LANGFUSE_SECRET_KEY",
|
|
3717
|
-
"LANGFUSE_BASE_URL",
|
|
3718
|
-
"LANGFUSE_TRACING_ENVIRONMENT"
|
|
3719
|
-
];
|
|
3720
|
-
function langfuseFragmentPath(home) {
|
|
3721
|
-
return join10(home, ".config", "agro", "langfuse.env");
|
|
3722
|
-
}
|
|
3723
|
-
function loadLangfuseCredentials(root) {
|
|
3724
|
-
const publicKey = readSecret(root, "LANGFUSE_PUBLIC_KEY");
|
|
3725
|
-
const secretKey = readSecret(root, "LANGFUSE_SECRET_KEY");
|
|
3726
|
-
if (publicKey !== void 0 && secretKey !== void 0) return { publicKey, secretKey };
|
|
3727
|
-
const missing = [
|
|
3728
|
-
publicKey === void 0 ? "LANGFUSE_PUBLIC_KEY" : void 0,
|
|
3729
|
-
secretKey === void 0 ? "LANGFUSE_SECRET_KEY" : void 0
|
|
3730
|
-
].filter((key) => key !== void 0);
|
|
3731
|
-
throw new Error(
|
|
3732
|
-
`${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not set in .env \u2014 run \`${activeBin()} secret set ${missing[0]} <value>\` first`
|
|
3733
|
-
);
|
|
3734
|
-
}
|
|
3735
|
-
var BARE_SAFE_VALUE = /^[A-Za-z0-9_.:/@%+=,-]+$/;
|
|
3736
|
-
var UNCARRIABLE_IN_VALUE = /[\u0000-\u001f\u007f']/;
|
|
3737
|
-
function renderLangfuseFragment(input) {
|
|
3738
|
-
const values = {
|
|
3739
|
-
LANGFUSE_PUBLIC_KEY: input.publicKey,
|
|
3740
|
-
LANGFUSE_SECRET_KEY: input.secretKey,
|
|
3741
|
-
LANGFUSE_BASE_URL: input.baseUrl,
|
|
3742
|
-
LANGFUSE_TRACING_ENVIRONMENT: input.environment
|
|
3743
|
-
};
|
|
3744
|
-
return LANGFUSE_FRAGMENT_KEYS.map((key) => `${key}=${quoteFragmentValue(key, values[key])}
|
|
3745
|
-
`).join("");
|
|
3746
|
-
}
|
|
3747
|
-
function writeLangfuseFragment(home, input) {
|
|
3748
|
-
return writeTextIfChanged(langfuseFragmentPath(home), renderLangfuseFragment(input), PRIVATE_FILE_MODE);
|
|
3749
|
-
}
|
|
3750
|
-
function quoteFragmentValue(key, value) {
|
|
3751
|
-
if (value === "") throw new Error(`${key} must not be empty`);
|
|
3752
|
-
if (UNCARRIABLE_IN_VALUE.test(value)) {
|
|
3753
|
-
throw new Error(
|
|
3754
|
-
`${key} must not contain a newline, a control character, or a single quote \u2014 zsh and systemd read the same fragment and neither carries that value safely`
|
|
3755
|
-
);
|
|
3756
|
-
}
|
|
3757
|
-
return BARE_SAFE_VALUE.test(value) ? value : `'${value}'`;
|
|
3758
|
-
}
|
|
3759
|
-
|
|
3760
3862
|
// src/commands/langfuse.ts
|
|
3761
3863
|
var DEFAULT_LANGFUSE_BASE_URL = "https://cloud.langfuse.com";
|
|
3762
3864
|
var LANGFUSE_FRAGMENT_LABEL = "~/.config/agro/langfuse.env";
|
|
@@ -4968,6 +5070,404 @@ async function runSandboxCommand(rest, bin) {
|
|
|
4968
5070
|
);
|
|
4969
5071
|
}
|
|
4970
5072
|
|
|
5073
|
+
// src/commands/egress.ts
|
|
5074
|
+
import { basename as basename7 } from "node:path";
|
|
5075
|
+
var EGRESS_LOG_PATH = "/var/log/squid/access.log";
|
|
5076
|
+
var EGRESS_LOG_LINES = 1e3;
|
|
5077
|
+
var PROXY_EXEC = ["exec", "-T", "proxy"];
|
|
5078
|
+
var EGRESS_COMMANDS = Object.freeze({
|
|
5079
|
+
log: Object.freeze([...PROXY_EXEC, "tail", "-n", String(EGRESS_LOG_LINES), EGRESS_LOG_PATH]),
|
|
5080
|
+
reload: Object.freeze([...PROXY_EXEC, "squid", "-k", "reconfigure"])
|
|
5081
|
+
});
|
|
5082
|
+
var RUNTIME_REFUSAL = Object.freeze({
|
|
5083
|
+
docker: void 0,
|
|
5084
|
+
openshell: (name) => `the sandbox ${name} uses runtime openshell; access.egress allowlist applies to the docker runtime`
|
|
5085
|
+
});
|
|
5086
|
+
function markDenied(text) {
|
|
5087
|
+
return text.split("\n").map((line) => line.includes("TCP_DENIED") ? `DENIED ${line}` : line).join("\n");
|
|
5088
|
+
}
|
|
5089
|
+
function refusal(root, name) {
|
|
5090
|
+
const runtimeRefusal = RUNTIME_REFUSAL[entryRuntime(root)];
|
|
5091
|
+
if (runtimeRefusal !== void 0) return runtimeRefusal(name);
|
|
5092
|
+
const egress = readAgroConfig(agroConfigPath(root)).access?.egress ?? "open";
|
|
5093
|
+
if (egress !== "allowlist") return `the sandbox ${name} uses access.egress ${egress}; set access.egress allowlist`;
|
|
5094
|
+
return void 0;
|
|
5095
|
+
}
|
|
5096
|
+
function rematerialize(root) {
|
|
5097
|
+
const repo = readAgroConfig(agroConfigPath(root)).repo;
|
|
5098
|
+
materialize(root, repo === void 0 || repo === "" ? {} : { repo });
|
|
5099
|
+
}
|
|
5100
|
+
function runEgress(verb, opts, io) {
|
|
5101
|
+
const run = opts.run ?? spawnRunner;
|
|
5102
|
+
const root = resolveSandboxRoot({ name: opts.name, cwd: opts.cwd });
|
|
5103
|
+
const name = basename7(root);
|
|
5104
|
+
const refused = refusal(root, name);
|
|
5105
|
+
if (refused !== void 0) {
|
|
5106
|
+
io.stderr(`${opts.bin} egress ${verb}: ${refused}
|
|
5107
|
+
`);
|
|
5108
|
+
return 1;
|
|
5109
|
+
}
|
|
5110
|
+
rematerialize(root);
|
|
5111
|
+
const script = requireLifecycleScript(root, "docker-compose.sh");
|
|
5112
|
+
const capture2 = verb === "log";
|
|
5113
|
+
return withComposeEnvFile(root, (extraArgs) => {
|
|
5114
|
+
const r = run("bash", [script, ...extraArgs, ...EGRESS_COMMANDS[verb]], {
|
|
5115
|
+
stdio: capture2 ? "capture" : "inherit"
|
|
5116
|
+
});
|
|
5117
|
+
assertSpawned(r, `bash ${script} egress ${verb}`);
|
|
5118
|
+
if (capture2) {
|
|
5119
|
+
if (r.stdout) io.stdout(markDenied(r.stdout));
|
|
5120
|
+
if (r.stderr) io.stderr(r.stderr);
|
|
5121
|
+
}
|
|
5122
|
+
return r.status ?? 1;
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5126
|
+
// src/command-table.ts
|
|
5127
|
+
var CONFIG_VERBS = ["show", "set", "repo"];
|
|
5128
|
+
var CONFIG_INTEGRATIONS = ["langfuse"];
|
|
5129
|
+
var SECRET_VERBS = ["set", "get", "list"];
|
|
5130
|
+
var LANGFUSE_VERBS = ["apply", "status", "disable"];
|
|
5131
|
+
var SANDBOX_SUBCOMMANDS = ["install", "list", "upgrade"];
|
|
5132
|
+
var WORKSPACE_SUBCOMMANDS = ["create", "list"];
|
|
5133
|
+
var HARNESS_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
|
|
5134
|
+
var TOOL_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
|
|
5135
|
+
var EGRESS_VERBS = ["log", "reload"];
|
|
5136
|
+
var NODE_VERBS = ["create", "list", "delete"];
|
|
5137
|
+
function isOneOf(values, value) {
|
|
5138
|
+
return value !== void 0 && values.includes(value);
|
|
5139
|
+
}
|
|
5140
|
+
var AGRO_COMMANDS = Object.freeze({
|
|
5141
|
+
config: [...CONFIG_VERBS, ...CONFIG_INTEGRATIONS],
|
|
5142
|
+
secret: SECRET_VERBS,
|
|
5143
|
+
langfuse: LANGFUSE_VERBS,
|
|
5144
|
+
update: null,
|
|
5145
|
+
"self-upgrade": null,
|
|
5146
|
+
vendor: null,
|
|
5147
|
+
sandbox: SANDBOX_SUBCOMMANDS,
|
|
5148
|
+
shell: null,
|
|
5149
|
+
compose: null,
|
|
5150
|
+
workspace: WORKSPACE_SUBCOMMANDS,
|
|
5151
|
+
harness: HARNESS_SUBCOMMANDS,
|
|
5152
|
+
tool: TOOL_SUBCOMMANDS,
|
|
5153
|
+
gateway: null,
|
|
5154
|
+
egress: EGRESS_VERBS,
|
|
5155
|
+
node: NODE_VERBS,
|
|
5156
|
+
...Object.fromEntries(composeVerbs().map((verb) => [verb, null]))
|
|
5157
|
+
});
|
|
5158
|
+
|
|
5159
|
+
// src/commands/node.ts
|
|
5160
|
+
var DEFAULT_CONSOLE_URL = "https://console.mifune.dev";
|
|
5161
|
+
var CONSOLE_CONFIG_MISSING = "set AGRO_CONSOLE_TOKEN, or run agro secret set AGRO_CONSOLE_TOKEN";
|
|
5162
|
+
var NODE_READY_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
5163
|
+
var NODE_POLL_INTERVAL_MS = 5e3;
|
|
5164
|
+
var FREE_TIER_SPEC = "n4";
|
|
5165
|
+
var FAILURE_STATUSES = /* @__PURE__ */ new Set(["failed", "destroy_requested", "destroying", "destroyed", "retired"]);
|
|
5166
|
+
function resolveConsoleConfig(env, readSecret2) {
|
|
5167
|
+
const lookup = (key) => {
|
|
5168
|
+
const fromEnv = env[key]?.trim();
|
|
5169
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
5170
|
+
const fromSecret = readSecret2(key)?.trim();
|
|
5171
|
+
return fromSecret === void 0 || fromSecret === "" ? void 0 : fromSecret;
|
|
5172
|
+
};
|
|
5173
|
+
const token = lookup("AGRO_CONSOLE_TOKEN");
|
|
5174
|
+
if (token === void 0) return void 0;
|
|
5175
|
+
const url = lookup("AGRO_CONSOLE_URL") ?? DEFAULT_CONSOLE_URL;
|
|
5176
|
+
return { url: url.replace(/\/+$/, ""), token };
|
|
5177
|
+
}
|
|
5178
|
+
function readProjectSecret(key) {
|
|
5179
|
+
try {
|
|
5180
|
+
return readSecret(resolveProjectRoot(), key);
|
|
5181
|
+
} catch {
|
|
5182
|
+
return void 0;
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
function defaultDeps2() {
|
|
5186
|
+
return {
|
|
5187
|
+
fetch: (input, init) => globalThis.fetch(input, init),
|
|
5188
|
+
env: process.env,
|
|
5189
|
+
readSecret: readProjectSecret,
|
|
5190
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
5191
|
+
now: () => Date.now(),
|
|
5192
|
+
pollIntervalMs: NODE_POLL_INTERVAL_MS,
|
|
5193
|
+
timeoutMs: NODE_READY_TIMEOUT_MS,
|
|
5194
|
+
confirm: (question) => confirm(question, false)
|
|
5195
|
+
};
|
|
5196
|
+
}
|
|
5197
|
+
function nodeHelp(bin) {
|
|
5198
|
+
return `${bin} node \u2014 Create, list, and delete agro-console sandbox nodes (remote VMs)
|
|
5199
|
+
|
|
5200
|
+
Usage:
|
|
5201
|
+
${bin} node create --spec <spec> --ssh-key <uuid> [--ssh-key <uuid>] [--name <name>] [--free-tier] [--json]
|
|
5202
|
+
${bin} node list [--json]
|
|
5203
|
+
${bin} node delete <id> [--yes]
|
|
5204
|
+
|
|
5205
|
+
\`create\` waits up to 15 minutes for the node to reach \`running\`, then prints
|
|
5206
|
+
its SSH command. \`--free-tier\` requests the console free tier. The spec
|
|
5207
|
+
defaults to n4. The verbs read AGRO_CONSOLE_URL and AGRO_CONSOLE_TOKEN from the
|
|
5208
|
+
environment, then from the project .env (\`${bin} secret set <KEY>\`).
|
|
5209
|
+
AGRO_CONSOLE_URL defaults to ${DEFAULT_CONSOLE_URL}.
|
|
5210
|
+
`;
|
|
5211
|
+
}
|
|
5212
|
+
function asRecord(value) {
|
|
5213
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
5214
|
+
}
|
|
5215
|
+
function asText(value) {
|
|
5216
|
+
if (typeof value === "string" && value !== "") return value;
|
|
5217
|
+
if (typeof value === "number") return String(value);
|
|
5218
|
+
return null;
|
|
5219
|
+
}
|
|
5220
|
+
function toNode(value) {
|
|
5221
|
+
const outer = asRecord(value);
|
|
5222
|
+
const record = asRecord(outer?.node) ?? outer;
|
|
5223
|
+
const id = asText(record?.id);
|
|
5224
|
+
if (record === void 0 || id === null) return void 0;
|
|
5225
|
+
return {
|
|
5226
|
+
id,
|
|
5227
|
+
name: asText(record.name),
|
|
5228
|
+
profile: asText(record.profile),
|
|
5229
|
+
status: asText(record.status) ?? "unknown",
|
|
5230
|
+
ipv4: asText(record.ipv4),
|
|
5231
|
+
ssh: asText(record.ssh)
|
|
5232
|
+
};
|
|
5233
|
+
}
|
|
5234
|
+
function sshCommand(node) {
|
|
5235
|
+
if (node.ssh !== null) return node.ssh;
|
|
5236
|
+
return node.ipv4 === null ? null : `ssh sandbox@${node.ipv4}`;
|
|
5237
|
+
}
|
|
5238
|
+
function errorDetail(status, body) {
|
|
5239
|
+
const record = asRecord(body);
|
|
5240
|
+
const nested = asRecord(record?.error);
|
|
5241
|
+
const code = asText(record?.code) ?? asText(nested?.code) ?? "no code";
|
|
5242
|
+
const message = asText(record?.error) ?? asText(nested?.message) ?? asText(record?.message);
|
|
5243
|
+
return `HTTP ${status} ${code}${message === null ? "" : `: ${message}`}`;
|
|
5244
|
+
}
|
|
5245
|
+
async function callApi(deps, config, method, path4, payload) {
|
|
5246
|
+
let response;
|
|
5247
|
+
try {
|
|
5248
|
+
response = await deps.fetch(`${config.url}${path4}`, {
|
|
5249
|
+
method,
|
|
5250
|
+
headers: {
|
|
5251
|
+
Authorization: `Bearer ${config.token}`,
|
|
5252
|
+
Accept: "application/json",
|
|
5253
|
+
...payload === void 0 ? {} : { "Content-Type": "application/json" }
|
|
5254
|
+
},
|
|
5255
|
+
...payload === void 0 ? {} : { body: JSON.stringify(payload) }
|
|
5256
|
+
});
|
|
5257
|
+
} catch (error) {
|
|
5258
|
+
return { ok: false, message: `request failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
5259
|
+
}
|
|
5260
|
+
const text = await response.text();
|
|
5261
|
+
let body = null;
|
|
5262
|
+
try {
|
|
5263
|
+
body = text === "" ? null : JSON.parse(text);
|
|
5264
|
+
} catch {
|
|
5265
|
+
body = null;
|
|
5266
|
+
}
|
|
5267
|
+
if (response.status >= 400) {
|
|
5268
|
+
return { ok: false, status: response.status, message: errorDetail(response.status, body) };
|
|
5269
|
+
}
|
|
5270
|
+
return { ok: true, status: response.status, body };
|
|
5271
|
+
}
|
|
5272
|
+
function parseCreate(tokens) {
|
|
5273
|
+
const args = { sshKeyIds: [], freeTier: false, json: false };
|
|
5274
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
5275
|
+
const token = tokens[i];
|
|
5276
|
+
if (token === "--json") {
|
|
5277
|
+
args.json = true;
|
|
5278
|
+
continue;
|
|
5279
|
+
}
|
|
5280
|
+
if (token === "--free-tier") {
|
|
5281
|
+
args.freeTier = true;
|
|
5282
|
+
continue;
|
|
5283
|
+
}
|
|
5284
|
+
const [flag, inline] = token.startsWith("--") && token.includes("=") ? [token.slice(0, token.indexOf("=")), token.slice(token.indexOf("=") + 1)] : [token, void 0];
|
|
5285
|
+
if (flag !== "--spec" && flag !== "--ssh-key" && flag !== "--name") {
|
|
5286
|
+
return { ok: false, error: token.startsWith("-") ? `unknown flag "${token}"` : `unexpected argument "${token}"` };
|
|
5287
|
+
}
|
|
5288
|
+
const value = inline ?? tokens[++i];
|
|
5289
|
+
if (value === void 0 || value === "") return { ok: false, error: `${flag} requires a value` };
|
|
5290
|
+
if (flag === "--spec") args.spec = value;
|
|
5291
|
+
else if (flag === "--name") args.name = value;
|
|
5292
|
+
else args.sshKeyIds.push(value);
|
|
5293
|
+
}
|
|
5294
|
+
if (args.spec === void 0 && args.freeTier) args.spec = FREE_TIER_SPEC;
|
|
5295
|
+
if (args.spec === void 0) return { ok: false, error: "--spec is required" };
|
|
5296
|
+
if (args.sshKeyIds.length === 0) return { ok: false, error: "at least one --ssh-key is required" };
|
|
5297
|
+
return { ok: true, args };
|
|
5298
|
+
}
|
|
5299
|
+
function printCreateResult(io, node, json) {
|
|
5300
|
+
const ssh = node.status === "running" ? sshCommand(node) : null;
|
|
5301
|
+
if (json) {
|
|
5302
|
+
io.stdout(`${JSON.stringify({ id: node.id, status: node.status, ssh })}
|
|
5303
|
+
`);
|
|
5304
|
+
return;
|
|
5305
|
+
}
|
|
5306
|
+
io.stdout(`id: ${node.id}
|
|
5307
|
+
status: ${node.status}
|
|
5308
|
+
${ssh === null ? "" : `${ssh}
|
|
5309
|
+
`}`);
|
|
5310
|
+
}
|
|
5311
|
+
async function runCreate(tokens, io, deps, config, bin) {
|
|
5312
|
+
const prefix = `${bin} node create`;
|
|
5313
|
+
const parsed = parseCreate(tokens);
|
|
5314
|
+
if (!parsed.ok) {
|
|
5315
|
+
io.stderr(`${prefix}: ${parsed.error}
|
|
5316
|
+
`);
|
|
5317
|
+
return 1;
|
|
5318
|
+
}
|
|
5319
|
+
const { spec, sshKeyIds, name, freeTier, json } = parsed.args;
|
|
5320
|
+
const created = await callApi(deps, config, "POST", "/api/nodes", {
|
|
5321
|
+
profile: "sandbox",
|
|
5322
|
+
spec,
|
|
5323
|
+
sshKeyIds,
|
|
5324
|
+
...name === void 0 ? {} : { name },
|
|
5325
|
+
...freeTier ? { freeTier: true } : {}
|
|
5326
|
+
});
|
|
5327
|
+
if (!created.ok) {
|
|
5328
|
+
io.stderr(`${prefix}: ${created.message}
|
|
5329
|
+
`);
|
|
5330
|
+
return 1;
|
|
5331
|
+
}
|
|
5332
|
+
let node = toNode(created.body);
|
|
5333
|
+
if (node === void 0) {
|
|
5334
|
+
io.stderr(`${prefix}: the console response has no node id
|
|
5335
|
+
`);
|
|
5336
|
+
return 1;
|
|
5337
|
+
}
|
|
5338
|
+
io.stderr(`${prefix}: node ${node.id} ${node.status}
|
|
5339
|
+
`);
|
|
5340
|
+
const deadline = deps.now() + deps.timeoutMs;
|
|
5341
|
+
while (node.status !== "running" && !FAILURE_STATUSES.has(node.status)) {
|
|
5342
|
+
const remaining = deadline - deps.now();
|
|
5343
|
+
if (remaining <= 0) {
|
|
5344
|
+
printCreateResult(io, node, json);
|
|
5345
|
+
io.stderr(`${prefix}: node ${node.id} is not running after 15 minutes \u2014 last status ${node.status}
|
|
5346
|
+
`);
|
|
5347
|
+
return 1;
|
|
5348
|
+
}
|
|
5349
|
+
await deps.sleep(Math.min(deps.pollIntervalMs, remaining));
|
|
5350
|
+
const polled = await callApi(deps, config, "GET", `/api/nodes/${encodeURIComponent(node.id)}`);
|
|
5351
|
+
if (!polled.ok) {
|
|
5352
|
+
io.stderr(`${prefix}: node ${node.id}: ${polled.message}
|
|
5353
|
+
`);
|
|
5354
|
+
return 1;
|
|
5355
|
+
}
|
|
5356
|
+
const previous = node.status;
|
|
5357
|
+
node = toNode(polled.body) ?? { ...node, status: "unknown" };
|
|
5358
|
+
if (node.status !== previous) io.stderr(`${prefix}: node ${node.id} ${node.status}
|
|
5359
|
+
`);
|
|
5360
|
+
}
|
|
5361
|
+
printCreateResult(io, node, json);
|
|
5362
|
+
if (node.status !== "running") {
|
|
5363
|
+
io.stderr(`${prefix}: node ${node.id} failed \u2014 last status ${node.status}
|
|
5364
|
+
`);
|
|
5365
|
+
return 1;
|
|
5366
|
+
}
|
|
5367
|
+
return 0;
|
|
5368
|
+
}
|
|
5369
|
+
function toNodeList(body) {
|
|
5370
|
+
const record = asRecord(body);
|
|
5371
|
+
const items = Array.isArray(body) ? body : record?.nodes ?? record?.data ?? record?.items;
|
|
5372
|
+
if (!Array.isArray(items)) return [];
|
|
5373
|
+
return items.flatMap((item) => toNode(item) ?? []);
|
|
5374
|
+
}
|
|
5375
|
+
async function runList(tokens, io, deps, config, bin) {
|
|
5376
|
+
const prefix = `${bin} node list`;
|
|
5377
|
+
const extra = tokens.find((token) => token !== "--json");
|
|
5378
|
+
if (extra !== void 0) {
|
|
5379
|
+
io.stderr(`${prefix}: ${extra.startsWith("-") ? "unknown flag" : "unexpected argument"} "${extra}"
|
|
5380
|
+
`);
|
|
5381
|
+
return 1;
|
|
5382
|
+
}
|
|
5383
|
+
const listed = await callApi(deps, config, "GET", "/api/nodes");
|
|
5384
|
+
if (!listed.ok) {
|
|
5385
|
+
io.stderr(`${prefix}: ${listed.message}
|
|
5386
|
+
`);
|
|
5387
|
+
return 1;
|
|
5388
|
+
}
|
|
5389
|
+
const rows = toNodeList(listed.body).map(({ id, name, profile, status, ipv4 }) => ({ id, name, profile, status, ipv4 }));
|
|
5390
|
+
if (tokens.includes("--json")) {
|
|
5391
|
+
io.stdout(`${JSON.stringify(rows)}
|
|
5392
|
+
`);
|
|
5393
|
+
return 0;
|
|
5394
|
+
}
|
|
5395
|
+
if (rows.length === 0) {
|
|
5396
|
+
io.stdout("no nodes\n");
|
|
5397
|
+
return 0;
|
|
5398
|
+
}
|
|
5399
|
+
const table = [
|
|
5400
|
+
["ID", "NAME", "PROFILE", "STATUS", "IPV4"],
|
|
5401
|
+
...rows.map((row) => [row.id, row.name ?? "-", row.profile ?? "-", row.status, row.ipv4 ?? "-"])
|
|
5402
|
+
];
|
|
5403
|
+
const widths = table[0].map((_, column) => Math.max(...table.map((cells) => cells[column].length)));
|
|
5404
|
+
for (const cells of table) {
|
|
5405
|
+
io.stdout(`${cells.map((cell3, column) => cell3.padEnd(widths[column])).join(" ").trimEnd()}
|
|
5406
|
+
`);
|
|
5407
|
+
}
|
|
5408
|
+
return 0;
|
|
5409
|
+
}
|
|
5410
|
+
async function runDelete(tokens, io, deps, config, bin) {
|
|
5411
|
+
const prefix = `${bin} node delete`;
|
|
5412
|
+
const flag = tokens.find((token) => token.startsWith("-") && token !== "--yes" && token !== "-y");
|
|
5413
|
+
if (flag !== void 0) {
|
|
5414
|
+
io.stderr(`${prefix}: unknown flag "${flag}"
|
|
5415
|
+
`);
|
|
5416
|
+
return 1;
|
|
5417
|
+
}
|
|
5418
|
+
const ids = tokens.filter((token) => !token.startsWith("-"));
|
|
5419
|
+
if (ids.length !== 1) {
|
|
5420
|
+
io.stderr(`${prefix}: takes exactly one node id
|
|
5421
|
+
`);
|
|
5422
|
+
return 1;
|
|
5423
|
+
}
|
|
5424
|
+
const [id] = ids;
|
|
5425
|
+
const yes = tokens.includes("--yes") || tokens.includes("-y");
|
|
5426
|
+
if (!yes && !await deps.confirm(`Destroy console node ${id}?`)) {
|
|
5427
|
+
io.stderr(`${prefix}: aborted \u2014 node ${id} unchanged
|
|
5428
|
+
`);
|
|
5429
|
+
return 1;
|
|
5430
|
+
}
|
|
5431
|
+
const deleted = await callApi(deps, config, "DELETE", `/api/nodes/${encodeURIComponent(id)}`);
|
|
5432
|
+
if (!deleted.ok) {
|
|
5433
|
+
io.stderr(
|
|
5434
|
+
deleted.status === 403 ? `${prefix}: destroy needs the admin role in agro-console
|
|
5435
|
+
` : `${prefix}: ${deleted.message}
|
|
5436
|
+
`
|
|
5437
|
+
);
|
|
5438
|
+
return 1;
|
|
5439
|
+
}
|
|
5440
|
+
io.stdout(`deleted ${id}
|
|
5441
|
+
`);
|
|
5442
|
+
return 0;
|
|
5443
|
+
}
|
|
5444
|
+
async function runNode(args, io, overrides = {}, bin = "agro") {
|
|
5445
|
+
const [verb, ...rest] = args;
|
|
5446
|
+
if (verb === void 0 || verb === "--help" || verb === "-h" || verb === "help") {
|
|
5447
|
+
io.stdout(nodeHelp(bin));
|
|
5448
|
+
return verb === void 0 ? 1 : 0;
|
|
5449
|
+
}
|
|
5450
|
+
if (!isOneOf(NODE_VERBS, verb)) {
|
|
5451
|
+
io.stderr(`${bin} node: unknown subcommand "${verb}" \u2014 expected ${NODE_VERBS.join(", ")}
|
|
5452
|
+
`);
|
|
5453
|
+
return 1;
|
|
5454
|
+
}
|
|
5455
|
+
if (rest.some((token) => token === "--help" || token === "-h")) {
|
|
5456
|
+
io.stdout(nodeHelp(bin));
|
|
5457
|
+
return 0;
|
|
5458
|
+
}
|
|
5459
|
+
const deps = { ...defaultDeps2(), ...overrides };
|
|
5460
|
+
const config = resolveConsoleConfig(deps.env, deps.readSecret);
|
|
5461
|
+
if (config === void 0) {
|
|
5462
|
+
io.stderr(`${bin} node ${verb}: ${CONSOLE_CONFIG_MISSING}
|
|
5463
|
+
`);
|
|
5464
|
+
return 1;
|
|
5465
|
+
}
|
|
5466
|
+
if (verb === "list") return runList(rest, io, deps, config, bin);
|
|
5467
|
+
if (verb === "delete") return runDelete(rest, io, deps, config, bin);
|
|
5468
|
+
return runCreate(rest, io, deps, config, bin);
|
|
5469
|
+
}
|
|
5470
|
+
|
|
4971
5471
|
// src/commands/harness.ts
|
|
4972
5472
|
import { existsSync as existsSync20 } from "node:fs";
|
|
4973
5473
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -4985,7 +5485,7 @@ function sourceDocsUrl(docsPath) {
|
|
|
4985
5485
|
|
|
4986
5486
|
// src/lib/host-config.ts
|
|
4987
5487
|
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4988
|
-
import { basename as
|
|
5488
|
+
import { basename as basename8, dirname as dirname8, isAbsolute, join as join15, resolve as resolve10 } from "node:path";
|
|
4989
5489
|
var HOST_CONFIG_MODE = 420;
|
|
4990
5490
|
var HOST_CONFIG_FILE = "config.json";
|
|
4991
5491
|
var WORKSPACES_SUBDIR = "workspaces";
|
|
@@ -5091,7 +5591,7 @@ function readHostConfig(env, home) {
|
|
|
5091
5591
|
try {
|
|
5092
5592
|
parsed = JSON.parse(raw);
|
|
5093
5593
|
} catch {
|
|
5094
|
-
throw new Error(`${
|
|
5594
|
+
throw new Error(`${basename8(path4)} is not valid JSON: ${path4}`);
|
|
5095
5595
|
}
|
|
5096
5596
|
return validateHostConfig(parsed);
|
|
5097
5597
|
}
|
|
@@ -5453,6 +5953,19 @@ async function validateHermesHome(target, io, bin, user, inheritedHome, root = s
|
|
|
5453
5953
|
}
|
|
5454
5954
|
return result.exitCode;
|
|
5455
5955
|
}
|
|
5956
|
+
async function seedHermes(target, io, bin, user, root = sandboxTargetRoot(target)) {
|
|
5957
|
+
const result = await target.exec({
|
|
5958
|
+
argv: remoteControlDirScript(root, "scripts/hermes-workspace.sh", ["seed", root, `${root}/.hermes`]),
|
|
5959
|
+
env: hermesEnv(root),
|
|
5960
|
+
...user ? { user } : {},
|
|
5961
|
+
stdio: "inherit"
|
|
5962
|
+
});
|
|
5963
|
+
if (result.exitCode !== 0) {
|
|
5964
|
+
io.stderr(`${bin} harness: could not write the Hermes defaults to ${root}/.hermes/config.yaml (exit ${result.exitCode}); nothing was installed.
|
|
5965
|
+
`);
|
|
5966
|
+
}
|
|
5967
|
+
return result.exitCode;
|
|
5968
|
+
}
|
|
5456
5969
|
async function configureHermes(target, io, bin, user, root = sandboxTargetRoot(target)) {
|
|
5457
5970
|
const result = await target.exec({
|
|
5458
5971
|
argv: remoteControlDirScript(root, "scripts/hermes-workspace.sh", [
|
|
@@ -5655,6 +6168,10 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
|
|
|
5655
6168
|
if (openclaw) openclawLaunch(io, root);
|
|
5656
6169
|
return 0;
|
|
5657
6170
|
}
|
|
6171
|
+
if (hermes) {
|
|
6172
|
+
const code = await seedHermes(target, io, bin, void 0, root);
|
|
6173
|
+
if (code !== 0) return code;
|
|
6174
|
+
}
|
|
5658
6175
|
io.stdout(`${present && hasReceipt ? "updating" : "installing"} ${entry.title} on the host\u2026
|
|
5659
6176
|
`);
|
|
5660
6177
|
const r = await target.exec({
|
|
@@ -5863,6 +6380,10 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
5863
6380
|
io.stderr("docker is required to install into the running sandbox but was not found on PATH\n");
|
|
5864
6381
|
return 1;
|
|
5865
6382
|
}
|
|
6383
|
+
if (hermes) {
|
|
6384
|
+
const code = await seedHermes(target, io, opts.bin, "sandbox");
|
|
6385
|
+
if (code !== 0) return code;
|
|
6386
|
+
}
|
|
5866
6387
|
if (already && await sandboxMarkerExists(target, entry)) {
|
|
5867
6388
|
io.stdout(`updating ${entry.title} in the sandbox\u2026
|
|
5868
6389
|
`);
|
|
@@ -6305,6 +6826,54 @@ install -d -m 0700 "$HOME/.tailscale"`
|
|
|
6305
6826
|
]),
|
|
6306
6827
|
docsPath: TOOLS_DOC
|
|
6307
6828
|
}),
|
|
6829
|
+
Object.freeze({
|
|
6830
|
+
id: "ffmpeg",
|
|
6831
|
+
title: "FFmpeg + FFprobe",
|
|
6832
|
+
kind: "installable",
|
|
6833
|
+
binary: "ffmpeg",
|
|
6834
|
+
verifyArgv: Object.freeze(["bash", "-lc", "command -v ffmpeg >/dev/null && command -v ffprobe >/dev/null"]),
|
|
6835
|
+
versionArgv: Object.freeze(["ffmpeg", "-version"]),
|
|
6836
|
+
installArgv: Object.freeze([
|
|
6837
|
+
"bash",
|
|
6838
|
+
"-lc",
|
|
6839
|
+
[
|
|
6840
|
+
"set -e",
|
|
6841
|
+
"release=b6.1.1",
|
|
6842
|
+
'case "$(dpkg --print-architecture)" in',
|
|
6843
|
+
" amd64) arch=x64; ffmpeg_sha=bfe8a8fc511530457b528c48d77b5737527b504a3797a9bc4866aeca69c2dffa; ffprobe_sha=25d9b6ccb05e3d9de9e04e31e2506d8dd7f9f0418981965ac6df12e8d3afd067 ;;",
|
|
6844
|
+
" arm64) arch=arm64; ffmpeg_sha=754a678672298bc68156adff58aa7385a592c2b30b1d0ae8750c45c915c4bac0; ffprobe_sha=2ab6aba60ee84412dff9188720703376cb4e7aaf7e0b5e43aa8249f2acae5bf8 ;;",
|
|
6845
|
+
' *) echo "no pinned FFmpeg build for $(dpkg --print-architecture)" >&2; exit 1 ;;',
|
|
6846
|
+
"esac",
|
|
6847
|
+
'prefix="${NPM_USER_PREFIX:-$HOME/.local}"',
|
|
6848
|
+
'tmp="$(mktemp -d)"',
|
|
6849
|
+
`trap 'rm -rf "$tmp"' EXIT`,
|
|
6850
|
+
"for name in ffmpeg ffprobe; do",
|
|
6851
|
+
' curl -fsSL "https://github.com/eugeneware/ffmpeg-static/releases/download/$release/$name-linux-$arch.gz" -o "$tmp/$name.gz"',
|
|
6852
|
+
"done",
|
|
6853
|
+
'echo "$ffmpeg_sha $tmp/ffmpeg.gz" | sha256sum -c -',
|
|
6854
|
+
'echo "$ffprobe_sha $tmp/ffprobe.gz" | sha256sum -c -',
|
|
6855
|
+
'install -d "$prefix/bin"',
|
|
6856
|
+
"for name in ffmpeg ffprobe; do",
|
|
6857
|
+
' gzip -dc "$tmp/$name.gz" > "$tmp/$name"',
|
|
6858
|
+
' install -m 0755 "$tmp/$name" "$prefix/bin/$name"',
|
|
6859
|
+
"done",
|
|
6860
|
+
'"$prefix/bin/ffprobe" -version >/dev/null',
|
|
6861
|
+
'if ! "$prefix/bin/ffmpeg" -hide_banner -encoders | grep -qw libx264; then',
|
|
6862
|
+
' echo "$prefix/bin/ffmpeg lists no libx264 encoder" >&2',
|
|
6863
|
+
" exit 1",
|
|
6864
|
+
"fi"
|
|
6865
|
+
].join("\n")
|
|
6866
|
+
]),
|
|
6867
|
+
installUser: "sandbox",
|
|
6868
|
+
hostCapable: true,
|
|
6869
|
+
uninstallArgv: Object.freeze([
|
|
6870
|
+
"rm",
|
|
6871
|
+
"-rf",
|
|
6872
|
+
`${HARNESS_PREFIX_TOKEN}/bin/ffmpeg`,
|
|
6873
|
+
`${HARNESS_PREFIX_TOKEN}/bin/ffprobe`
|
|
6874
|
+
]),
|
|
6875
|
+
docsPath: TOOLS_DOC
|
|
6876
|
+
}),
|
|
6308
6877
|
Object.freeze({
|
|
6309
6878
|
id: "docker-engine",
|
|
6310
6879
|
title: "Docker Engine + Compose",
|
|
@@ -7098,35 +7667,6 @@ function fetchRemoteSource(opts) {
|
|
|
7098
7667
|
return opts.destDir;
|
|
7099
7668
|
}
|
|
7100
7669
|
|
|
7101
|
-
// src/command-table.ts
|
|
7102
|
-
var CONFIG_VERBS = ["show", "set", "repo"];
|
|
7103
|
-
var CONFIG_INTEGRATIONS = ["langfuse"];
|
|
7104
|
-
var SECRET_VERBS = ["set", "list"];
|
|
7105
|
-
var LANGFUSE_VERBS = ["apply", "status", "disable"];
|
|
7106
|
-
var SANDBOX_SUBCOMMANDS = ["install", "list", "upgrade"];
|
|
7107
|
-
var WORKSPACE_SUBCOMMANDS = ["create", "list"];
|
|
7108
|
-
var HARNESS_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
|
|
7109
|
-
var TOOL_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
|
|
7110
|
-
function isOneOf(values, value) {
|
|
7111
|
-
return value !== void 0 && values.includes(value);
|
|
7112
|
-
}
|
|
7113
|
-
var AGRO_COMMANDS = Object.freeze({
|
|
7114
|
-
config: [...CONFIG_VERBS, ...CONFIG_INTEGRATIONS],
|
|
7115
|
-
secret: SECRET_VERBS,
|
|
7116
|
-
langfuse: LANGFUSE_VERBS,
|
|
7117
|
-
update: null,
|
|
7118
|
-
"self-upgrade": null,
|
|
7119
|
-
vendor: null,
|
|
7120
|
-
sandbox: SANDBOX_SUBCOMMANDS,
|
|
7121
|
-
shell: null,
|
|
7122
|
-
compose: null,
|
|
7123
|
-
workspace: WORKSPACE_SUBCOMMANDS,
|
|
7124
|
-
harness: HARNESS_SUBCOMMANDS,
|
|
7125
|
-
tool: TOOL_SUBCOMMANDS,
|
|
7126
|
-
gateway: null,
|
|
7127
|
-
...Object.fromEntries(composeVerbs().map((verb) => [verb, null]))
|
|
7128
|
-
});
|
|
7129
|
-
|
|
7130
7670
|
// src/cli.ts
|
|
7131
7671
|
var DEFAULT_SOURCE_CONTROL_DIR = resolve13(
|
|
7132
7672
|
dirname10(fileURLToPath(import.meta.url)),
|
|
@@ -7161,7 +7701,7 @@ Usage:
|
|
|
7161
7701
|
${bin} sandbox <args...> Create, list, and upgrade sandboxes (install|list|upgrade)
|
|
7162
7702
|
${bin} shell [name] Open a zsh shell in the running sandbox container
|
|
7163
7703
|
${bin} config <args...> Read and write ${stateNames(bin).configFile} (show|set), or run a wizard
|
|
7164
|
-
${bin} secret <args...> Read and write the gitignored root .env (set|list)
|
|
7704
|
+
${bin} secret <args...> Read and write the gitignored root .env (set|get|list)
|
|
7165
7705
|
${bin} langfuse <args...> Render, check, or disable Langfuse tracing files (apply|status|disable)
|
|
7166
7706
|
${bin} self-upgrade Upgrade the installed ${bin} CLI
|
|
7167
7707
|
${bin} vendor Vendor or upgrade the ${stateNames(bin).controlDir}/ control plane
|
|
@@ -7171,7 +7711,9 @@ Usage:
|
|
|
7171
7711
|
${bin} ps [name] Show sandbox service status
|
|
7172
7712
|
${bin} destroy [name] Remove the sandbox and wipe its named volumes
|
|
7173
7713
|
${bin} compose config Print the resolved docker compose configuration
|
|
7714
|
+
${bin} egress <args...> Read the egress proxy log or reload its allowlist (log|reload)
|
|
7174
7715
|
${bin} workspace <args...> Create and list host AGRO workspaces (create|list)
|
|
7716
|
+
${bin} node <args...> Manage remote agro-console sandbox nodes (create|list|delete)
|
|
7175
7717
|
${bin} harness <args...> Install and inspect agent CLI harnesses
|
|
7176
7718
|
${bin} tool <args...> Install and inspect sandbox tooling
|
|
7177
7719
|
${bin} gateway <args...> Manage a messaging client session (pi|hermes|openclaw)
|
|
@@ -7214,14 +7756,16 @@ function printSecretHelp(bin = AGRO_PRODUCT.bin) {
|
|
|
7214
7756
|
|
|
7215
7757
|
Usage:
|
|
7216
7758
|
${bin} secret set <KEY> [--sandbox <name>] Prompt for the value (input hidden)
|
|
7759
|
+
${bin} secret get <KEY> [--sandbox <name>] Print the raw value
|
|
7217
7760
|
${bin} secret list [--sandbox <name>] List the keys that hold a value
|
|
7218
7761
|
|
|
7219
|
-
Without --sandbox
|
|
7220
|
-
the registry entry \`${bin} sandbox list\` names.
|
|
7762
|
+
Without --sandbox all three verbs act on the equipped project root; with it they
|
|
7763
|
+
act on the registry entry \`${bin} sandbox list\` names.
|
|
7221
7764
|
|
|
7222
7765
|
The value is never read from the command line \u2014 an argument would land in your
|
|
7223
|
-
shell history. \`${bin} secret
|
|
7224
|
-
|
|
7766
|
+
shell history. \`${bin} secret get\` is the only command that prints a raw value;
|
|
7767
|
+
\`${bin} secret list\` never does. .env is mode 0600 and gitignored; every non-secret
|
|
7768
|
+
setting belongs in ${stateNames(bin).configFile} (\`${bin} config set\`).
|
|
7225
7769
|
|
|
7226
7770
|
Keys:
|
|
7227
7771
|
${secretKeyList()}
|
|
@@ -7236,7 +7780,7 @@ Usage:
|
|
|
7236
7780
|
${bin} langfuse disable Set langfuse.enabled=false, delete the credential fragment, rewrite the files
|
|
7237
7781
|
|
|
7238
7782
|
\`${bin} config langfuse\` is the interactive wizard that writes these settings.
|
|
7239
|
-
\`apply\` never prompts and never installs anything; bootstrap
|
|
7783
|
+
\`apply\` never prompts and never installs anything; the bootstrap runs it.
|
|
7240
7784
|
It reads langfuse.* from ${stateNames(bin).configFile} and the keys from the gitignored .env,
|
|
7241
7785
|
and writes nothing when langfuse.enabled is not true. The generated files live in the
|
|
7242
7786
|
sandbox home, so \`apply\` and \`disable\` refuse to touch them from the host.
|
|
@@ -7279,7 +7823,7 @@ Flags:
|
|
|
7279
7823
|
--dry-run Report the installation kind, target, and versions without
|
|
7280
7824
|
changing anything.
|
|
7281
7825
|
|
|
7282
|
-
Project payload vendoring (${stateNames(bin).controlDir}/
|
|
7826
|
+
Project payload vendoring (${stateNames(bin).controlDir}/) is \`${bin} vendor\`; its --from,
|
|
7283
7827
|
--from-remote, --ref and --force flags are not accepted here.
|
|
7284
7828
|
`);
|
|
7285
7829
|
}
|
|
@@ -7289,7 +7833,7 @@ function printVendorHelp(bin = AGRO_PRODUCT.bin) {
|
|
|
7289
7833
|
Usage:
|
|
7290
7834
|
${bin} vendor [--from <dir> | --from-remote [--ref <ref>]] [--dry-run] [--force]
|
|
7291
7835
|
|
|
7292
|
-
Writes ONLY the ${stateNames(bin).controlDir}/ control plane
|
|
7836
|
+
Writes ONLY the ${stateNames(bin).controlDir}/ control plane (skills, scripts, CLI) into the
|
|
7293
7837
|
current directory. An empty directory is equipped from scratch; everything else
|
|
7294
7838
|
in your project is left untouched \u2014 it writes no ${stateNames(bin).configFile}, .env, AGENTS.md,
|
|
7295
7839
|
.gitignore or .devcontainer/.
|
|
@@ -7488,6 +8032,38 @@ Flags:
|
|
|
7488
8032
|
See ${sourceDocsUrl("docs/lifecycle-commands.md")} for the full mapping.
|
|
7489
8033
|
`);
|
|
7490
8034
|
}
|
|
8035
|
+
function printEgressHelp(bin = AGRO_PRODUCT.bin) {
|
|
8036
|
+
process.stdout.write(`${bin} egress \u2014 Read the egress proxy log or reload its allowlist
|
|
8037
|
+
|
|
8038
|
+
Usage:
|
|
8039
|
+
${bin} egress log [name] Print the last ${EGRESS_LOG_LINES} lines of the proxy access log; prefix denied lines with DENIED
|
|
8040
|
+
${bin} egress reload [name] Rewrite egress-allow.txt and send the proxy \`squid -k reconfigure\`
|
|
8041
|
+
|
|
8042
|
+
Both verbs run ${stateNames(bin).controlDir}/scripts/docker-compose.sh \`exec proxy\` in the sandbox entry.
|
|
8043
|
+
The log is ${EGRESS_LOG_PATH} in the proxy container. \`reload\` keeps the sandbox running.
|
|
8044
|
+
Both verbs need a docker sandbox with access.egress allowlist; any other sandbox exits 1.
|
|
8045
|
+
[name] is a sandbox entry from \`${bin} sandbox list\`; with exactly one registered sandbox it can be omitted.
|
|
8046
|
+
|
|
8047
|
+
See ${sourceDocsUrl("docs/lifecycle-commands.md")} for every verb.
|
|
8048
|
+
`);
|
|
8049
|
+
}
|
|
8050
|
+
function parseEgressArgs(rest, bin = AGRO_PRODUCT.bin) {
|
|
8051
|
+
if (rest.length === 0 || isHelpFlag2(rest[0])) return { ok: true, args: { help: true } };
|
|
8052
|
+
const [head, ...tail] = rest;
|
|
8053
|
+
if (!isOneOf(EGRESS_VERBS, head)) {
|
|
8054
|
+
return {
|
|
8055
|
+
ok: false,
|
|
8056
|
+
error: `${bin} egress: unknown subcommand "${head}" \u2014 expected ${EGRESS_VERBS.join(" or ")}`,
|
|
8057
|
+
showHelp: true
|
|
8058
|
+
};
|
|
8059
|
+
}
|
|
8060
|
+
if (isHelpFlag2(tail[0])) return { ok: true, args: { help: true } };
|
|
8061
|
+
if (tail.length > 0 && tail[0].startsWith("-")) {
|
|
8062
|
+
return { ok: false, error: `${bin} egress ${head}: unknown flag "${tail[0]}"` };
|
|
8063
|
+
}
|
|
8064
|
+
if (tail.length > 1) return { ok: false, error: `${bin} egress ${head}: unexpected argument "${tail[1]}"` };
|
|
8065
|
+
return { ok: true, args: tail.length === 0 ? { help: false, verb: head } : { help: false, verb: head, name: tail[0] } };
|
|
8066
|
+
}
|
|
7491
8067
|
function printComposeHelp(bin = AGRO_PRODUCT.bin) {
|
|
7492
8068
|
process.stdout.write(`${bin} compose \u2014 Inspect the resolved docker compose setup
|
|
7493
8069
|
|
|
@@ -7603,7 +8179,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
|
|
|
7603
8179
|
if (!SECRET_VERBS.includes(head)) {
|
|
7604
8180
|
return {
|
|
7605
8181
|
ok: false,
|
|
7606
|
-
error: `${bin} secret: unknown subcommand "${head}" \u2014 expected set or
|
|
8182
|
+
error: `${bin} secret: unknown subcommand "${head}" \u2014 expected set, list, or get`,
|
|
7607
8183
|
showHelp: true
|
|
7608
8184
|
};
|
|
7609
8185
|
}
|
|
@@ -7617,12 +8193,12 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
|
|
|
7617
8193
|
}
|
|
7618
8194
|
const [key, ...extra] = tail;
|
|
7619
8195
|
if (key === void 0) {
|
|
7620
|
-
return { ok: false, error: `${bin} secret
|
|
8196
|
+
return { ok: false, error: `${bin} secret ${verb}: a key is required`, showHelp: true };
|
|
7621
8197
|
}
|
|
7622
8198
|
if (extra.length > 0) {
|
|
7623
8199
|
return {
|
|
7624
8200
|
ok: false,
|
|
7625
|
-
error: `${bin} secret set: takes only a key \u2014 the value is prompted for, never passed on the command line where your shell history would keep it`
|
|
8201
|
+
error: verb === "get" ? `${bin} secret get: takes only a key` : `${bin} secret set: takes only a key \u2014 the value is prompted for, never passed on the command line where your shell history would keep it`
|
|
7626
8202
|
};
|
|
7627
8203
|
}
|
|
7628
8204
|
return { ok: true, args: { ...args, verb, key } };
|
|
@@ -8095,6 +8671,7 @@ async function main(argv) {
|
|
|
8095
8671
|
};
|
|
8096
8672
|
const scope = a.sandbox === void 0 ? { bin } : { bin, sandbox: a.sandbox };
|
|
8097
8673
|
if (a.verb === "list") return await runSecretList(scope, io);
|
|
8674
|
+
if (a.verb === "get") return await runSecretGet(a.key, scope, io);
|
|
8098
8675
|
return await runSecretSet(a.key, scope, io);
|
|
8099
8676
|
}
|
|
8100
8677
|
if (first === "langfuse") {
|
|
@@ -8219,6 +8796,29 @@ async function main(argv) {
|
|
|
8219
8796
|
}
|
|
8220
8797
|
return runComposeConfig({ bin }, parsed.args.passthrough);
|
|
8221
8798
|
}
|
|
8799
|
+
if (first === "egress") {
|
|
8800
|
+
const parsed = parseEgressArgs(argv.slice(1), bin);
|
|
8801
|
+
if (!parsed.ok) {
|
|
8802
|
+
process.stderr.write(`${parsed.error}
|
|
8803
|
+
`);
|
|
8804
|
+
if (parsed.showHelp) printEgressHelp(bin);
|
|
8805
|
+
return 1;
|
|
8806
|
+
}
|
|
8807
|
+
const a = parsed.args;
|
|
8808
|
+
if (a.help || a.verb === void 0) {
|
|
8809
|
+
printEgressHelp(bin);
|
|
8810
|
+
return second === void 0 ? 1 : 0;
|
|
8811
|
+
}
|
|
8812
|
+
return runEgress(a.verb, a.name === void 0 ? { bin } : { bin, name: a.name }, lifecycleIo());
|
|
8813
|
+
}
|
|
8814
|
+
if (first === "node") {
|
|
8815
|
+
return await runNode(
|
|
8816
|
+
argv.slice(1),
|
|
8817
|
+
{ stdout: (s) => process.stdout.write(s), stderr: (s) => process.stderr.write(s) },
|
|
8818
|
+
{},
|
|
8819
|
+
bin
|
|
8820
|
+
);
|
|
8821
|
+
}
|
|
8222
8822
|
if (first === "workspace") {
|
|
8223
8823
|
const parsed = parseWorkspaceArgs(argv.slice(1), bin);
|
|
8224
8824
|
if (!parsed.ok) {
|
|
@@ -8400,6 +9000,7 @@ export {
|
|
|
8400
9000
|
parseComposeArgs,
|
|
8401
9001
|
parseConfigArgs,
|
|
8402
9002
|
parseDestroyArgs,
|
|
9003
|
+
parseEgressArgs,
|
|
8403
9004
|
parseGatewayArgs,
|
|
8404
9005
|
parseHarnessArgs,
|
|
8405
9006
|
parseLangfuseArgs,
|
|
@@ -8414,6 +9015,7 @@ export {
|
|
|
8414
9015
|
printComposeHelp,
|
|
8415
9016
|
printComposeVerbHelp,
|
|
8416
9017
|
printDestroyHelp,
|
|
9018
|
+
printEgressHelp,
|
|
8417
9019
|
printGatewayHelp,
|
|
8418
9020
|
printHarnessHelp,
|
|
8419
9021
|
printLangfuseHelp,
|