@tiens.nguyen/gonext-local-worker 1.0.225 → 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.
- package/gonext-local-worker.mjs +6 -0
- package/gonext-repl.mjs +51 -6
- package/gonext_agent_chat.py +52 -18
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -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,
|
package/gonext-repl.mjs
CHANGED
|
@@ -699,6 +699,21 @@ async function runAgentTurn(history) {
|
|
|
699
699
|
let thinkWord = pickWord(); // playful word shown UNDER the in-progress line (flavor)
|
|
700
700
|
let almostDone = false; // set from the worker's "…almost completed thinking…" heartbeat
|
|
701
701
|
let runningCmd = null; // the command currently executing (from a "Running → …" event)
|
|
702
|
+
// The model's CURRENT Thought (task #64): while it streams its reasoning inside the
|
|
703
|
+
// hidden ~~~ fence, show a one-clause summary on the blinking line so a 300s CPU-bound
|
|
704
|
+
// step reads as "◑ Adding the nav links to app.component.html… (52s)" instead of a
|
|
705
|
+
// random word. `fenceThought` accumulates the raw in-fence text for the current step.
|
|
706
|
+
let liveThought = "";
|
|
707
|
+
let fenceThought = "";
|
|
708
|
+
// First non-empty Thought clause: drop the "Thought:" prefix, stop at the <code> block
|
|
709
|
+
// (we never show code), take the first line, and cap length so it never wraps.
|
|
710
|
+
const extractThought = (buf) => {
|
|
711
|
+
let t = String(buf || "");
|
|
712
|
+
const ci = t.search(/<code[\s>]/i);
|
|
713
|
+
if (ci >= 0) t = t.slice(0, ci);
|
|
714
|
+
t = t.replace(/^\s*Thought:\s*/i, "").split(/\n/)[0].trim();
|
|
715
|
+
return t;
|
|
716
|
+
};
|
|
702
717
|
const thinkSecs = () => Math.max(1, Math.round((Date.now() - thinkingSince) / 1000));
|
|
703
718
|
|
|
704
719
|
// The status is TWO in-place lines — the in-progress action, then the playful word
|
|
@@ -724,9 +739,12 @@ async function runAgentTurn(history) {
|
|
|
724
739
|
const glyph = SPINNER[secs % SPINNER.length]; // rotates → reads as "in progress"
|
|
725
740
|
// Line 1 = WHAT is happening now: the running command, else the phase (Thinking /
|
|
726
741
|
// Almost done). Line 2 = the playful word (flavor). Truncate so neither wraps.
|
|
742
|
+
// Priority: a live command > the model's current Thought clause (task #64, so a
|
|
743
|
+
// long CPU-bound step reads as what it's actually reasoning about, not "Thinking")
|
|
744
|
+
// > the "almost done" heartbeat > the bare fallback.
|
|
727
745
|
const primary = runningCmd
|
|
728
746
|
? `Running ${runningCmd}`
|
|
729
|
-
: almostDone ? "Almost done" : "Thinking";
|
|
747
|
+
: liveThought || (almostDone ? "Almost done" : "Thinking");
|
|
730
748
|
const max = Math.max(24, (process.stdout.columns || 80) - 14);
|
|
731
749
|
const fit = (s) => (s.length > max ? s.slice(0, max - 1) + "…" : s);
|
|
732
750
|
clearStatus();
|
|
@@ -744,6 +762,10 @@ async function runAgentTurn(history) {
|
|
|
744
762
|
clearStatus();
|
|
745
763
|
thinking = false;
|
|
746
764
|
runningCmd = null;
|
|
765
|
+
// A concrete action landed → the Thought that led to it is spent; clear it so the
|
|
766
|
+
// next step's blinking line starts from "Thinking", not last step's stale clause.
|
|
767
|
+
liveThought = "";
|
|
768
|
+
fenceThought = "";
|
|
747
769
|
lastContentAt = Date.now();
|
|
748
770
|
};
|
|
749
771
|
|
|
@@ -782,11 +804,26 @@ async function runAgentTurn(history) {
|
|
|
782
804
|
almostDone = /almost/i.test(line);
|
|
783
805
|
continue;
|
|
784
806
|
}
|
|
785
|
-
|
|
786
|
-
//
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
807
|
+
// A fence opening starts a fresh step's raw stream → reset the harvested Thought
|
|
808
|
+
// so the blinking line doesn't carry last step's clause into this one.
|
|
809
|
+
if (isFenceLine(line)) {
|
|
810
|
+
inStreamFence = !inStreamFence;
|
|
811
|
+
if (inStreamFence) fenceThought = "";
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
// Inside a ~~~ fence = raw model Thought/code stream → suppressed from scrollback. Do
|
|
815
|
+
// NOT bump lastContentAt: while the (now hidden) tokens flow, the blinking bullet keeps
|
|
816
|
+
// ticking so the user sees the step is still being worked on. But HARVEST the first
|
|
817
|
+
// Thought clause for the blinking line (task #64) — up to the <code> block, which is
|
|
818
|
+
// where the prose ends and the tool call begins (we never surface code here).
|
|
819
|
+
if (inStreamFence) {
|
|
820
|
+
if (!/<code[\s>]/i.test(fenceThought)) {
|
|
821
|
+
fenceThought += line + "\n";
|
|
822
|
+
const th = extractThought(fenceThought);
|
|
823
|
+
if (th) liveThought = th;
|
|
824
|
+
}
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
790
827
|
if (isToolStepSummary(line)) { lastContentAt = Date.now(); continue; } // swallow
|
|
791
828
|
if (isRoutingLine(line)) {
|
|
792
829
|
// Swallow the router's play-by-play entirely — the blinking-bullet ticker (which
|
|
@@ -914,6 +951,14 @@ async function runAgentTurn(history) {
|
|
|
914
951
|
answerShownLive = true;
|
|
915
952
|
lastWasBlank = false;
|
|
916
953
|
}
|
|
954
|
+
// While in the hidden fence, the model's FIRST Thought line often sits in `carry` with
|
|
955
|
+
// no newline yet (a long reasoning sentence streams token-by-token) — recompute the
|
|
956
|
+
// live clause from the partial too, so the blinking line updates mid-sentence instead
|
|
957
|
+
// of only once the line finally breaks (task #64).
|
|
958
|
+
if (inStreamFence && !/<code[\s>]/i.test(fenceThought)) {
|
|
959
|
+
const th = extractThought(fenceThought + carry);
|
|
960
|
+
if (th) liveThought = th;
|
|
961
|
+
}
|
|
917
962
|
// A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
|
|
918
963
|
// a newline), so tokens are actively flowing — keep the ticker asleep.
|
|
919
964
|
if (carry.trim() !== "") lastContentAt = Date.now();
|
package/gonext_agent_chat.py
CHANGED
|
@@ -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
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
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
|
-
|
|
2964
|
-
"
|
|
2965
|
-
"
|
|
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
|
|
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
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
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.
|
|
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",
|