Adds synapse/slash_commands.py: a chat message that's nothing but
/tool_name(arg=val, arg=val) dispatches straight through tools.dispatch(),
skipping model selection, RAG/playbook context assembly, and the ask-policy
approval round-trip entirely. A human typing this IS the approval - there's
no one else to ask - so it's a deliberate, reviewed bypass of the approval
step specifically, not of anything a tool validates internally (path
boundaries, size caps, Curry's own sandbox checks all still run). Argument
values parse via ast.literal_eval only: strings/numbers/bools/None/literal
containers, no names, no calls, no attribute access - a malformed or
hostile-looking argument fails to parse rather than executing anything.
Wired into chat_stream_endpoint (main.py) as an early short-circuit, before
any of the RAG/model-selection work that a slash-command doesn't need. Web
needed no changes (it already forwards raw text unchanged); the TUI
previously swallowed every leading "/" locally and never reached the backend
with it, so tui_app.py's _handle_slash now falls through to _start_chat for
anything shaped like a tool call while still handling its own local
meta-commands (/help, /model, /new, ...) exactly as before.
Also finally wires Curry in as ten real tools (curry_declare_constant,
curry_get_constant/_latest, curry_list_constants, curry_retire_constant,
curry_declare_function, curry_get_function, curry_list_functions,
curry_call_function, curry_retire_function) - deferred from the vendoring
pass. The five write/execute ones are ACTION tools in the same
always-ask-regardless-of-global-policy floor as edit_source
(ALWAYS_ASK_ACTION_TOOLS, generalized in tools.py from the old
self_edit-only ALWAYS_ASK_TOOLS so future tool families share one place to
register into). curry_call_function is gated as an action for the same
reason run_snippet is: it executes code, even sandboxed.
Fixed a real bug surfaced while wiring this up: curry_db is a long-lived
singleton holding one sqlite3 connection (unlike NexusOS's own memory store,
which opens/closes a fresh connection per call specifically to dodge this),
and sqlite3 forbids using a connection from a different thread than created
it. That's a non-issue in production (uvicorn's single event-loop thread),
but Starlette's TestClient runs the ASGI app through an anyio portal thread,
so it broke immediately under test. Fixed at the source (curry_core.py,
Curry.__init__) with check_same_thread=False, documented as a second
deliberate vendoring deviation alongside the PR #4 sandbox fix - there was
never real concurrent access here, just an overly strict same-thread
assertion tripping on a thread-identity change with only one logical caller.
Verified: 244 backend tests pass (18 new for the parser + endpoint wiring +
curry tool registration, 4 new for the TUI passthrough); the 12 pre-existing
C/C++/Rust toolchain failures are unrelated and unchanged. Confirmed by hand
over the real HTTP endpoint: successful dispatch, zero tool_request events
(approval bypass working as designed), a format()-dunder exploit attempt
still rejected by the vendored sandbox fix even through the new tool
registration, malformed arguments rejected before ever reaching dispatch,
and an unknown tool name rejected cleanly. Wheel rebuilt and content-checked
(bin/check.sh's gate now also asserts slash_commands.py ships).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vendors curry_core.py from Athena-Pro/Curry (with the str.format()/format_map()
sandbox-escape fix from https://github.com/Athena-Pro/Curry/pull/4 already
applied) into synapse/, since Curry itself isn't a pip-installable package -
it's meant to be pointed at via a config path, which only works from a source
checkout. Vendoring a single self-contained, stdlib-only file ships it inside
NexusOS's own wheel with no extra dependency to reconcile.
synapse/curry_store.py opens it into a module-level singleton (curry_db) at
import time, the same pattern as memory.store.store and
playbooks.store.playbook_store, and main.py imports it so it's genuinely
initialized at process startup - preloaded, not lazy-on-first-use. Backed by
its own CURRY_DB file (nexus_config.py), separate from memory.db.
NexusOS builds exactly one wheel (py3-none-any, no compiled extensions) -
there is no separate Windows/macOS/Linux artifact; platform differences are
handled by requirement overlays at install time, not by building different
wheels. Verified the same wheel actually carries this correctly: built it,
confirmed twine check passes, confirmed synapse/curry_core.py and
curry_store.py are present in the archive (bin/check.sh's packaging gate now
asserts this too), then installed that exact wheel into a throwaway venv and
round-tripped a declare_constant/get_constant_latest call against it with no
source checkout present - proving "preloaded and ready to be called" holds
from the shipped artifact, not just editable-install execution.
Android/Termux is unaffected by this change in either direction: it already
has a separate, documented, pre-existing blocker in docs/TERMUX.md (no
published Android pydantic-core wheel) that has nothing to do with Curry,
which is pure stdlib and adds no new native/binary dependency.
Scope: preload only, nothing wired into a chat-facing tool yet - no model or
user-authored content reaches declare_function/call_function today.
Verified: 216 backend tests pass (4 new in test_curry_store.py, including a
regression test proving the vendored sandbox fix survived the copy); the 12
pre-existing C/C++/Rust toolchain failures are unrelated and unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
install-macos.sh mirrors install.sh's split: every portable step - git pull,
venv, pip with the right overlay, npm build - stays in bin/sync.py, shared
with Linux and Windows. The script only does what sync.py cannot do for
itself on a bare Mac, which is install the Homebrew packages needed before a
Python exists to run sync.py with.
Two stages had to learn about darwin. ensure_exec_bits() keyed off
`os.name == "nt"`, which is false on macOS, so it ran the Linux path; and
requirements() had no darwin branch. linux_stage() now no-ops there, which is
what makes skipping the Ollama fetch correct rather than an omission:
bin/fetch-ollama.sh only ships a Linux x86-64 binary, and _ollama_bin() in
synapse/ollama_manager.py already prefers the bundled copy and falls back to
whatever `ollama` is on PATH. On macOS that is the brewed one, with Metal
acceleration and no flags needed.
The XFCE desktop branding is Linux-only and was already gated off macOS the
same way, so there is nothing to install for it here.
install-macos.sh joins the shell-parse list in bin/check.sh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI shipped from `management/`, which also holds desktop-only pieces (the
Tk control panel, the XFCE panel wiring, the shell wrappers). Packaging that
directory meant the wheel either dragged in tkinter or shipped a broken import.
Split it: `nexusos_cli/` is what the wheel ships and what `nexus`/`ncp`/
`nexusos` dispatch to, `management/` keeps the desktop half.
Alongside the move:
* hatch_build.py decides the interface/web/dist include at build time. dist/
is gitignored, so a static force-include aborts `pip install -e .` on a
fresh clone - before the reader reaches the `npm run build` step. Editable
installs now skip a missing dist; wheels and sdists hard-error naming the
command to run.
* synapse/proc_util.py gives frontend_manager and ncp process inspection and
termination without psutil, which became an optional extra when the wheel
landed. It routes around Windows having no signals, where os.kill(pid, 15)
is an unblockable TerminateProcess rather than a polite request.
* nexusos_cli/monitor.py adds `ncp monitor`, an ASCII dashboard with no curses
or rich dependency so it works in Termux, plain SSH and Windows Terminal.
Collector and renderer are separate so tests feed fixtures, no stack needed.
* tests/test_packaging_deps.py fails the gate when synapse or nexusos_cli
import a distribution pyproject does not declare, and when an optional
dependency is imported at module scope instead of lazily.
* bin/check.sh now builds the wheel, twine-checks it, and asserts the compiled
UI and seed playbooks are actually inside it. A wheel that builds but ships
no dist/ serves a blank page, which only shows up after release.
tests/test_nexus_api.py moves to tests/ with the module it covers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bin/install.sh still rsynced --delete from a backup path retired in July, so
the documented Linux install both failed and could erase a working tree. Every
step it claimed to do already lives in bin/sync.py, shared with Windows.
Root ./install.sh is now a thin wrapper over `sync.py restore`, so deployment
stays one bash command. It also registers ncp/promethean in ~/.bashrc, which
was the only thing the old script uniquely did.
Split restore-linux.sh into runtime (Ollama binary, shell aliases) and desktop
(XFCE panel, theme, os-release). Only desktop writes outside the repo, it now
auto-skips off XFCE, and `--no-desktop` skips it explicitly. Two tests keep the
stage names in sync and the $HOME writes confined to the desktop stage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>