Gives the assistant three new ACTION tools to change its own playbooks,
runtime settings, and (source checkout only) its own source code, all
reusing the existing run_snippet/remember approval framework — but with
a hardcoded floor (self_edit.ALWAYS_ASK_TOOLS) so these three always pause
for per-call human approval regardless of the global action_tool_policy
setting. Flipping that policy for an unrelated tool must never silently
also unlock unattended self-modification.
synapse/self_edit.py is the new module doing the actual work, documented
in the same explicit "here's what is and isn't a security boundary" style
as code_run.py:
- edit_source is confined to settings.project_root via the same
realpath + Path.parents boundary check that just closed a sibling-
directory bypass in /icons/image, plus a denylist of dangerous
subtrees (.git, the venv, node_modules, build output, runtime state).
Gated on settings.source_checkout — refuses cleanly in a wheel
install, where there's no live repo to edit or commit into.
- The model sends full file content, never a diff; the server computes
the diff itself via difflib against what's actually on disk, so a
human reviews ground truth, not a description the model wrote.
- Every applied source edit best-effort commits to git as an audit
trail — independent of, not a substitute for, the approval gate.
- edit_playbook merges instead of replacing (main.py's prior
_persist_playbook did a raw replace, which was only safe because the
frontend form always sent a complete object — unsafe for a tool a
model calls with a partial argument set, so this also fixes that
latent bug). Becoming the active system prompt requires an explicit
make_active flag, never a side effect of an ordinary edit.
- edit_settings reuses the existing _SETTINGS_DEFAULTS allowlist.
The approval UI (Chatbot.jsx) previously rendered a tool call's arguments
as Object.values(args).join(", ") in a single-line badge — unusable for
reviewing a diff. It now renders a real, server-computed preview (diff
for source, before/after for playbook/settings) via a new shared
diff-view.js helper, with a loud banner when a change would become the
active system prompt or touch action_tool_policy/system_prompt. A new
nexus-edit fence (self-edit-langs.js + Markdown.jsx's EditBlock) shows
the same diff after an edit is applied, mirroring nexus-run.
Verified: 212 backend tests pass (29 new in test_self_edit.py; the 12
pre-existing C/C++/Rust toolchain failures are unrelated and unchanged),
57 frontend node:test cases pass (15 new), eslint and vite build clean,
and the full approval-preview render path was exercised against the real
built UI with a mocked SSE stream covering all four preview branches
(source diff, playbook becomes-main, settings policy-change, and a
rejected/failing preview).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
/icons/image used a bare string startswith() against allowed roots, so
a sibling dir like /usr/share/icons_evil would pass as if it were under
/usr/share/icons. Switched to the pathlib parents-based check already
used correctly in icons/compositor.py, plus a regression test.
Also stopped three bare `except Exception: pass` blocks (auto model
select fallback, conversation titling) from swallowing errors silently
- they now log to the existing chat trace helper. Behavior unchanged,
just visible when something's actually failing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Close SQLite handles safely on Windows, clean orphaned vectors, normalize Ollama endpoints, and surface model errors without leaking reasoning tags.
Co-authored-by: Cursor <cursoragent@cursor.com>
DoS/quota guardrails for the unauthenticated local APIs:
* Body-size middleware rejects oversized requests (Content-Length) before they
are buffered/base64-decoded (NEXUS_MAX_REQUEST_MB, default 32).
* Document upload enforces a decoded-byte cap (NEXUS_MAX_UPLOAD_MB, default 20)
and a PDF page-count cap (NEXUS_MAX_PDF_PAGES, default 500) as backstops for
chunked bodies and pathological files.
* A counter-based in-flight limiter bounds concurrent chats and document
ingests (NEXUS_MAX_CONCURRENT_CHATS/UPLOADS), returning 429 when saturated;
the chat slot is held for the whole SSE stream and released on completion or
client disconnect.
* /models/pull gains an opt-in allowlist (NEXUS_MODEL_ALLOWLIST); empty by
default so behaviour is unchanged, otherwise a bare repo name permits all its
tags.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two tool/agent-layer hardening changes:
* fetch_url now resolves the target host and refuses to connect if any
resolved address is loopback, private (RFC1918/ULA), link-local (incl. the
169.254.169.254 cloud-metadata endpoint), multicast, reserved, or
unspecified. IPv4-mapped IPv6 is unwrapped first, and the guard re-runs on
every redirect hop so a public URL cannot 302 its way to an internal target.
* /chat/approve now requires a single-use token minted when the stream pauses
for approval and delivered only in that stream's tool_request event, compared
in constant time. Previously the pending approval was keyed solely on a
client-supplied conversation_id, so anyone who could enumerate a
conversation_id could approve another client's pending action.
The frontend threads the token from the tool_request event into the approve
call.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Synapse backend and memory service bound 0.0.0.0 with wildcard CORS and no
auth, exposing the full unauthenticated admin/data API to the LAN. Default the
uvicorn bind to 127.0.0.1 (NEXUS_BIND_HOST override), scope CORS to known local
origins instead of "*", and add TrustedHostMiddleware to reject foreign Host
headers (which defeats DNS-rebinding, something same-origin CORS cannot stop).
NEXUS_ALLOWED_HOSTS / NEXUS_ALLOWED_ORIGINS allow opt-in LAN exposure, intended
to be paired with real authentication.
Co-authored-by: Cursor <cursoragent@cursor.com>