@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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/agro.js +716 -114
  3. 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, skills, and crons
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/ + crons/ — and nothing else (compatibility window)
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/` + `crons/` into the current directory, equipping an empty checkout and upgrading an
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
- rl.question(` ${question} `, (answer) => {
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:-test1234}\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 cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\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';
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:-test1234}\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 cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\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';
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 dockerFiles(entry, opts) {
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
- return [
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 files = entryRuntime(entry) === "openshell" ? openshellFiles(entry) : dockerFiles(entry, opts);
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.20.0" : "0.0.0-dev";
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}=${redact(value)}
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" : redact(value)}
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 join9(home, ".claude", "settings.json");
3612
+ return join10(home, ".claude", "settings.json");
3464
3613
  }
3465
3614
  function piTracingConfigPath(home) {
3466
- return join9(home, ".pi", "agent", "langfuse.json");
3615
+ return join10(home, ".pi", "agent", "langfuse.json");
3467
3616
  }
3468
3617
  function codexTracingConfigPath(home) {
3469
- return join9(home, ".codex", "langfuse.json");
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 basename7, dirname as dirname8, isAbsolute, join as join15, resolve as resolve10 } from "node:path";
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(`${basename7(path4)} is not valid JSON: ${path4}`);
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 both verbs write the equipped project root; with it they write
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 list\` never prints a raw value. .env is mode 0600 and
7224
- gitignored; every non-secret setting belongs in ${stateNames(bin).configFile} (\`${bin} config set\`).
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, systemd, and cron run it.
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}/ + crons/) is \`${bin} vendor\`; its --from,
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 and crons/ (skills, scripts, CLI) into the
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 list`,
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 set: a key is required`, showHelp: true };
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "AGRO CLI — create and manage sandboxes for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/mifunedev/agro#readme",