@tiens.nguyen/gonext-local-worker 1.0.222 → 1.0.224

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.
@@ -2202,12 +2202,23 @@ def _ws_record_change(action: str, path: str, backup) -> None:
2202
2202
  _log(f"workspace manifest write failed: {e}")
2203
2203
 
2204
2204
 
2205
- # Command prefixes run_command may execute (first token after shlex split). Build/test
2206
- # runners only no shells, no package publishes, no sudo.
2205
+ # Command prefixes run_command may execute (first token after shlex split). This is a
2206
+ # STEERING mechanism, not a sandbox: python3 (allowed) can subprocess anything, and
2207
+ # ssh/rsync flags (ProxyCommand, -e) can local-exec too. A run-enabled workspace means
2208
+ # the user trusts the agent with local-user-level execution — the list exists to keep
2209
+ # the model on productive paths (build/test/deploy), not to contain a hostile one.
2210
+ # Still excluded on purpose: bare shells, package publishes, sudo, and bare `pip`
2211
+ # (the model must use `python3 -m pip`, which pip itself recommends).
2207
2212
  _WS_RUN_ALLOWED = {
2208
2213
  "npm", "npx", "yarn", "pnpm", "node", "mvn", "./mvnw", "gradle", "./gradlew",
2209
2214
  "pytest", "python", "python3", "go", "cargo", "make", "dotnet", "swift",
2210
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",
2211
2222
  }
2212
2223
 
2213
2224
  # ---- background servers (behavior-detected, command-agnostic) ----
@@ -2339,7 +2350,10 @@ def _ws_scrubbed_env() -> dict:
2339
2350
  them, so we pass an explicit whitelist instead of inheriting."""
2340
2351
  import os
2341
2352
  keep = ("PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "SHELL", "USER", "TERM",
2342
- "JAVA_HOME", "GOPATH", "CARGO_HOME", "NVM_DIR")
2353
+ "JAVA_HOME", "GOPATH", "CARGO_HOME", "NVM_DIR",
2354
+ # SSH agent socket so ssh/scp/rsync can authenticate with the user's KEY
2355
+ # (no password in the model's context) for deploys (task #57).
2356
+ "SSH_AUTH_SOCK", "SSH_AGENT_PID")
2343
2357
  env = {k: os.environ[k] for k in keep if k in os.environ}
2344
2358
  # CI=1 makes watch-mode test runners (CRA's `npm test`, jest --watch) run ONCE and
2345
2359
  # exit instead of sitting in interactive watch mode until the timeout kills them.
@@ -2927,6 +2941,21 @@ def run_agent_chat(cfg):
2927
2941
  "left RUNNING (its window opens on the user's Mac); stop_server closes it. An "
2928
2942
  "interactive input() CLI can't be driven here (EOFError) — make it non-interactive "
2929
2943
  "or give the user the command.\n"
2944
+ f" ALLOWED runners (first word only): {', '.join(sorted(_WS_RUN_ALLOWED))}. "
2945
+ "Anything else is rejected — do NOT retry a blocked command a different way. "
2946
+ "For Python packages use `python3 -m pip install <pkg>` (bare `pip` is NOT allowed).\n"
2947
+ " DEPLOY (ssh/scp/rsync are allowed): transfer files + run remote commands "
2948
+ "directly, e.g. `rsync -az build/ user@host:/path` then `ssh user@host '…'`. Use "
2949
+ "NON-INTERACTIVE KEY auth: add `-o BatchMode=yes` (and "
2950
+ "`-o StrictHostKeyChecking=accept-new`) so it fails fast instead of hanging on a "
2951
+ "prompt. NEVER put a password in a command, file, or your reply — there is no "
2952
+ "stdin here. If the server only accepts a PASSWORD (BatchMode fails with "
2953
+ "'Permission denied (publickey,password)'), STOP and tell the user to set up key "
2954
+ "auth once: `ssh-copy-id user@host` — then retry. Do not write paramiko/expect "
2955
+ "scripts with hardcoded credentials. REMOTE sudo usually FAILS over ssh (no TTY: "
2956
+ "'a terminal is required') — deploy to a directory the remote user can write, "
2957
+ "and if a root step is unavoidable (mv to /var/www, nginx config/reload), "
2958
+ "final_answer the exact one-liner for the user to run on the server themselves.\n"
2930
2959
  " NOTE: plain `import os`/`open()` is blocked — use ONLY these tools for files.\n"
2931
2960
  " WRITING FILE CONTENT: multi-line content MUST use \\n for newlines (or a "
2932
2961
  "triple-quoted string). NEVER put a raw line break inside a normal \"...\" — it "
@@ -4496,7 +4525,7 @@ def run_agent_chat(cfg):
4496
4525
 
4497
4526
  @tool
4498
4527
  def run_command(command: str, workdir: str = "", timeout_seconds: int = 180) -> str:
4499
- """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. 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.
4528
+ """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.
4500
4529
 
4501
4530
  Args:
4502
4531
  command: the command line (allowlisted runners only; no shells or sudo).
@@ -4509,7 +4538,27 @@ def run_agent_chat(cfg):
4509
4538
  import signal
4510
4539
  import subprocess
4511
4540
  import time as _t
4512
- wd = _rag_read_allowed((workdir or "").strip() or _default_ws_root())
4541
+ # workdir="" the active workspace. Guard the common model mistake of
4542
+ # passing the workspace's OWN name (e.g. workdir="t13" while the active
4543
+ # folder is /…/t13), which would double-nest to /…/t13/t13
4544
+ # (FileNotFoundError). EXISTENCE-FIRST: a real subdir always wins — only
4545
+ # strip the redundant leading segment when the nested path does NOT exist
4546
+ # (so a legit repo/repo layout like web/web is never misrouted). Handles
4547
+ # both the bare form ("t13") and the prefixed form ("t13/my-react-app").
4548
+ _wd_in = (workdir or "").strip()
4549
+ if _wd_in and not os.path.isabs(os.path.expanduser(_wd_in)):
4550
+ _active = _default_ws_root()
4551
+ if _wd_in in (".", "./"):
4552
+ _wd_in = ""
4553
+ elif not os.path.isdir(os.path.join(_active, _wd_in)):
4554
+ _base = os.path.basename(_active)
4555
+ if _wd_in == _base:
4556
+ _wd_in = ""
4557
+ elif _wd_in.startswith(_base + os.sep):
4558
+ _stripped = _wd_in[len(_base) + 1:]
4559
+ if os.path.isdir(os.path.join(_active, _stripped)):
4560
+ _wd_in = _stripped
4561
+ wd = _rag_read_allowed(_wd_in or _default_ws_root())
4513
4562
  w = _ws_for_path(wd)
4514
4563
  if w is None or not w.get("allowRun"):
4515
4564
  msg = ("Error: running commands is not enabled for this workspace. "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.222",
3
+ "version": "1.0.224",
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",