@tiens.nguyen/gonext-local-worker 1.0.226 → 1.0.227

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.
@@ -1842,6 +1842,12 @@ async function runAgentChatJob(job) {
1842
1842
  // workspaces are registered. Passing a literal 5 here would look user-set and
1843
1843
  // block that bump.
1844
1844
  maxSteps: payload?.maxSteps ?? 0,
1845
+ // run_command policy (web Settings → Agent). Both optional: empty = allow every
1846
+ // command except the always-blocked sudo/su. runAllowlist (non-empty) = opt-in
1847
+ // lockdown to exactly those runners; runDenylist = extra blocks. Accept whatever
1848
+ // the API sends (list or comma/space string); python re-parses either shape.
1849
+ runAllowlist: payload?.runAllowlist ?? [],
1850
+ runDenylist: payload?.runDenylist ?? [],
1845
1851
  // Max web_search + fetch_url calls the agent may make per task (user-configurable
1846
1852
  // in web Settings → Agent). Default 10; past it the retrieval tools refuse.
1847
1853
  researchBudget: payload?.researchBudget ?? 10,
@@ -2209,17 +2209,23 @@ def _ws_record_change(action: str, path: str, backup) -> None:
2209
2209
  # the model on productive paths (build/test/deploy), not to contain a hostile one.
2210
2210
  # Still excluded on purpose: bare shells, package publishes, sudo, and bare `pip`
2211
2211
  # (the model must use `python3 -m pip`, which pip itself recommends).
2212
- _WS_RUN_ALLOWED = {
2213
- "npm", "npx", "yarn", "pnpm", "node", "mvn", "./mvnw", "gradle", "./gradlew",
2214
- "pytest", "python", "python3", "go", "cargo", "make", "dotnet", "swift",
2215
- "rspec", "bundle", "php", "composer", "tsc", "jest", "vitest",
2216
- # Deployment (task #57): transfer files + run remote commands. Auth uses the user's
2217
- # own SSH key/agent (SSH_AUTH_SOCK passes through _ws_scrubbed_env); the model is
2218
- # told to use non-interactive KEY auth (BatchMode) and NEVER hardcode passwords (a
2219
- # stdin password prompt EOFErrors here). No `sftp`: it's interactive by default
2220
- # (would hang timeout kill); scp/rsync cover the same ground batch-safely.
2221
- "ssh", "scp", "rsync",
2222
- }
2212
+ # run_command execution policy — ALLOW-BY-DEFAULT.
2213
+ # We used to gate on a fixed allowlist (npm/python/go/…). It rejected legitimate DevOps
2214
+ # tooling (docker, kubectl, terraform, aws, gh, …) and, because that universe is unbounded
2215
+ # and grows constantly, any list was a hardcode that upset the next user. Crucially, the
2216
+ # gate was never a SANDBOX: `python3`/`node` are runnable and can subprocess anything, so a
2217
+ # restrictive allowlist bought STEERING, not safety it just added friction. So now: any
2218
+ # command runs, with two guards kept:
2219
+ # 1. Privilege escalation is ALWAYS blocked (`sudo`/`su`/`doas`) running the model's
2220
+ # commands as root is the one footgun a run-enabled workspace shouldn't hand over.
2221
+ # 2. Commands run via argv (shlex.split + Popen, NO shell), so pipes/redirects/`&&`
2222
+ # /backticks are literal args, never shell operations — the shell-injection guard.
2223
+ # The user can still tighten this from web Settings (passed through cfg, both default off):
2224
+ # • runAllowlist — if non-empty, ONLY these runners are permitted (opt-in lockdown).
2225
+ # • runDenylist — extra runners to block on top of the always-blocked set.
2226
+ _WS_RUN_DENY_ALWAYS = {"sudo", "su", "doas"} # privilege escalation — never runnable
2227
+ _WS_RUN_ALLOWLIST = set() # opt-in lockdown from cfg; empty = allow all
2228
+ _WS_RUN_DENYLIST = set() # extra user blocks from cfg
2223
2229
 
2224
2230
  # ---- background servers (behavior-detected, command-agnostic) ----
2225
2231
  # run_command classifies a command by OBSERVED BEHAVIOR, never by its text: if the
@@ -2597,6 +2603,18 @@ def run_agent_chat(cfg):
2597
2603
  except Exception: # noqa: BLE001
2598
2604
  continue
2599
2605
  _WS_AVAILABLE = bool(_WS_ROOTS)
2606
+ # run_command policy overrides from Settings (both optional, default = allow all but
2607
+ # sudo). Accept either a list or a comma/space-separated string; basenames so a user
2608
+ # can write "git" and match "/usr/bin/git".
2609
+ global _WS_RUN_ALLOWLIST, _WS_RUN_DENYLIST
2610
+
2611
+ def _split_cmds(v):
2612
+ import re as _re
2613
+ items = v if isinstance(v, (list, tuple)) else _re.split(r"[,\s]+", str(v or ""))
2614
+ return {str(c).strip() for c in items if str(c).strip()}
2615
+
2616
+ _WS_RUN_ALLOWLIST = _split_cmds(cfg.get("runAllowlist"))
2617
+ _WS_RUN_DENYLIST = _split_cmds(cfg.get("runDenylist"))
2600
2618
  # Active terminal workspace: the folder the `gonext` REPL was launched from (sent as
2601
2619
  # payload.activeWorkspace = the terminal's cwd). download_file / unzip_file put their
2602
2620
  # output HERE instead of the shared ~/.gonext/rag-work cache, so files land where the
@@ -2960,9 +2978,10 @@ def run_agent_chat(cfg):
2960
2978
  "left RUNNING (its window opens on the user's Mac); stop_server closes it. An "
2961
2979
  "interactive input() CLI can't be driven here (EOFError) — make it non-interactive "
2962
2980
  "or give the user the command.\n"
2963
- f" ALLOWED runners (first word only): {', '.join(sorted(_WS_RUN_ALLOWED))}. "
2964
- "Anything else is rejected do NOT retry a blocked command a different way. "
2965
- "For Python packages use `python3 -m pip install <pkg>` (bare `pip` is NOT allowed).\n"
2981
+ " Most commands run directly build/test/package tools, git, docker, kubectl, "
2982
+ "terraform, aws/gcloud/az, and so on. Two limits: `sudo`/`su` are blocked (no root); "
2983
+ "and there is NO shell, so pipes `|`, redirects `>`, `&&`, and backticks do NOT work "
2984
+ "— run one command per call, or use python3 for anything that needs composition.\n"
2966
2985
  " - deploy_web(local_dir, host, user, remote_path, domain='') — DEPLOY a built "
2967
2986
  "static site in ONE call: build first (run_command('npm run build')), then "
2968
2987
  "deploy_web('my-app/build', host, user, remote_path, domain). It rsyncs over your "
@@ -4568,7 +4587,7 @@ def run_agent_chat(cfg):
4568
4587
  """Run a command inside a workspace that has run permission (e.g. 'npm test', 'npm run build', 'npm start', 'mvn test', 'python3 app.py'). One-shot commands return their output when they exit. If the command turns out to be a SERVER (keeps running and listens on a port), it is left RUNNING and its URL is reported. If it is a DESKTOP/GUI app (opens a window — tkinter, pygame, PyQt…), it is LAUNCHED and left running: its window appears on the user's Mac; tell them it's open. Both are stoppable via stop_server. Also allows DEPLOY commands (ssh/scp/rsync) — use non-interactive KEY auth (-o BatchMode=yes); never put a password in the command (there is no stdin). Note: an interactive CLI that reads stdin (input()) cannot be driven here — it will fail with EOFError; make it non-interactive or give the user the command to run themselves.
4569
4588
 
4570
4589
  Args:
4571
- command: the command line (allowlisted runners only; no shells or sudo).
4590
+ command: the command line. Most commands run; no shell (so no pipes/redirects/&&), and sudo/su are blocked.
4572
4591
  workdir: directory to run in. Empty = first registered workspace.
4573
4592
  timeout_seconds: give up after this many seconds if the command neither exits nor starts a server (max 600).
4574
4593
  """
@@ -4610,9 +4629,24 @@ def run_agent_chat(cfg):
4610
4629
  msg = "Error: empty command."
4611
4630
  _last_obs["text"] = msg
4612
4631
  return msg
4613
- if argv[0] not in _WS_RUN_ALLOWED:
4614
- msg = (f"Error: '{argv[0]}' is not an allowed runner. Allowed: "
4615
- + ", ".join(sorted(_WS_RUN_ALLOWED)))
4632
+ # Allow-by-default policy (see _WS_RUN_DENY_ALWAYS). Match on the runner's
4633
+ # basename too, so `/usr/bin/sudo` can't slip past a bare-name block.
4634
+ _runner = argv[0]
4635
+ _rbase = os.path.basename(_runner)
4636
+ if _runner in _WS_RUN_DENY_ALWAYS or _rbase in _WS_RUN_DENY_ALWAYS:
4637
+ msg = (f"Error: '{_runner}' is blocked (privilege escalation is never "
4638
+ "run by the agent). Run that step yourself, or tell the user the "
4639
+ "exact command to run.")
4640
+ _last_obs["text"] = msg
4641
+ return msg
4642
+ if _WS_RUN_DENYLIST and (_runner in _WS_RUN_DENYLIST or _rbase in _WS_RUN_DENYLIST):
4643
+ msg = (f"Error: '{_runner}' is blocked by your run denylist "
4644
+ "(change it in web Settings → Agent).")
4645
+ _last_obs["text"] = msg
4646
+ return msg
4647
+ if _WS_RUN_ALLOWLIST and _runner not in _WS_RUN_ALLOWLIST and _rbase not in _WS_RUN_ALLOWLIST:
4648
+ msg = (f"Error: '{_runner}' is not in your run allowlist. Add it in web "
4649
+ "Settings → Agent, or clear the allowlist to allow all commands.")
4616
4650
  _last_obs["text"] = msg
4617
4651
  return msg
4618
4652
  _emit({"type": "step", "text": f"Running → {command[:70]}"})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.226",
3
+ "version": "1.0.227",
4
4
  "description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
5
5
  "type": "module",
6
6
  "license": "MIT",