| 2026-11-08 |

sessions: share a chat as a public read-only link
...
A session's owner can hand out one capability link that opens the display
history without an account: no input, no chat list, no artifacts panel.
The token lives in the URL hash, so it never reaches the server in a request
line or a Referer header.
The link is permanent by decision - no expiry, no revoke. Deleting the chat
removes the share row (FK cascade) and closes it, which is the only switch.
Backend: session_shares table (unique token, FK cascade), POST
/sessions/{id}/share issuing the session's one link (owner or admin; a
read-all permission may read another user's chat but not publish it), and
public GET /shared/{token}[/messages] serving display history only, with
session and owner ids left out of the payload.
Webclient: #share=<token> renders a standalone page with the owner's cards,
rating off, interactive UI components filtered out and scroll-up paging; a
share button in the chat header opens a dialog that creates the link.
Also fixes ChatHeader's rename, which called an undefined `api` and failed
silently.
Eugene Sukhodolskiy
committed
7 hours ago
|
llm: rotate the routing config under a running process
...
A key that lives in the central config is only worth centralizing if
publishing it does not cost a fleet restart. The backend bakes host and
Authorization header into its clients, so it now accepts a fresh
LLMConfig: clients are dropped and rebuilt on the next call, health
stays keyed by target name, and the probe follows the new hosts too.
The overlay's llm section is no longer always staged. It is re-parsed
against the local document and swapped live; only a backend without the
swap method or a document that fails to parse falls back to
restart_required.
Eugene Sukhodolskiy
committed
7 hours ago
|
| 2026-11-07 |

navi: overlay the hive config onto the instance's own
...
An instance that knows a hive with the swarm key starts from the hive
document: layers that apply to it are merged over its local config, fields
the hive does not mention stay local, and an empty answer means 'run exactly
your own config'. Settings, profiles, LLM routes and MCP servers are covered;
database URL, ports, auth switches and the channel's own settings can never be
overridden.
Startup fetches with a short timeout, falls back to the last validated overlay
on disk and refuses to crash-loop on a document that killed the previous start.
A live refresh applies secrets and timeouts in place (the frozen singleton is
written through one deliberate escape hatch), rebuilds profiles from their
local base and reports everything else as restart_required. The hive reaches
the instance by push, by the announce reply and by a retry sweep; every apply
is acked back, which is what the hive's status view reads.
Eugene Sukhodolskiy
committed
20 hours ago
|
profiles: teach the agent to scout profiles and tools before switching
...
Persona gains a reconnaissance order (list_profiles, then list_tools for the
real toolset, then tool_manual) and the switch-vs-delegate rule: switch when
the task itself is another profile's work, delegate a side piece with
spawn_agent. The profiles block is built in one place and shared with the
admin prompt debug view, and the stale 'next user message' claim is fixed.
list_tools applies the admin-only role gate to an explicitly named profile,
the same one list_profiles uses, so it cannot enumerate hidden profiles.
Eugene Sukhodolskiy
committed
1 day ago
|
prompt: give the methodologies catalog its rules in the system prompt
Eugene Sukhodolskiy
committed
1 day ago
|
profiles: give every working profile the skills and protocol tools
Eugene Sukhodolskiy
committed
1 day ago
|

protocol: run a protocol as a committed plan, with a log of how it ended
...
A protocol was only text until now. The new `protocol` tool starts one: its
steps are spliced into the session's todo plan after the agent's current
position (nothing already done is reordered), a row appears in `protocol_runs`
with a snapshot of the version, steps and post-checks, and the run stays
`running` until it is closed.
`finish` refuses while any step of the run is open, refuses outright when one
failed, and requires a report — it is the run's record for the user. `cancel`
and `fail` require a reason and mark the rest of the run's steps `skipped`, so
the plan does not keep work that was never done. One run per session is active
(a partial unique index enforces it).
The commitment also has to survive the context: every model turn while a run is
open carries a system reminder with the protocol, its steps and the way to close
it, and the final-turn intercept now fires for an open run even when the todo
shows no open steps, so a bare-text turn cannot leave one running.
`protocol` is wired into assistant, developer, discuss and server_admin next to
`skills`; sub-agents get neither, because a protocol belongs to the session's
plan. The run log has no UI yet — `protocol(op='status')` is the current view.
Eugene Sukhodolskiy
committed
1 day ago
|

skills: agent-written skills and protocol proposals with chat buttons
...
A skill the agent writes now lands in the catalog on its own; a protocol
never does. `skills(op="propose")` stores the document in `skill_proposals`
and returns a card through the tool result's `ui_component` metadata — no
`render_component` call, so profiles without `navi_ui` get the card too.
The decision is the server's: the browser sends `skill_proposal` over the
WebSocket, `SkillStore.decide_proposal` writes the catalog row from the
*stored* document, names the clicking user as the author, and answers with
`skill_proposal_decided`. The agent learns the outcome as a hidden user
message, the same path a `navi_ui` form submission takes.
`create`/`update` refuse protocol documents and point at `propose` with
`replaces_id`, so the version history of a protocol always starts from a
user's yes. The chat store now renders `ui_component` from any tool whose
result carries one, not only from `mcp__navi_ui__render_component`.
Eugene Sukhodolskiy
committed
1 day ago
|

skills: catalog of skills and protocols with a settings tab
...
One registry for reusable methodologies: markdown with YAML front matter,
stored in PostgreSQL alongside a full edit history. A row is either a
`skill` (advice, adapt as needed) or a `protocol` (a committed order of
actions), and a protocol is refused on save unless it declares its steps,
post-checks and the three escape hatches — on_deviation, on_blocked,
on_already_done — because an honest run needs a way out when reality
differs from the plan.
- `SkillStore`: lazy DDL, uniqueness on (owner_id, kind, name), version
bump with an appended `skill_versions` row, `content_hash` as the only
hive-sync hook. Personal rows carry an owner; shared rows have an empty
one and are writable by an admin.
- REST `/skills` (list/get/create/update/delete/versions) and the agent
tool `skills` (search/load) that renders a compact document — numbered
steps and escape hatches for a protocol, the instructions for a skill.
- Settings → Скилы и протоколы: catalog with search and kind/area
filters, a document editor that shows the server's validation error
verbatim, create from a template, delete, edit history, and the
personal/shared switch for an admin.
Manuals stay where they are: tool knowledge, separate from this catalog.
Eugene Sukhodolskiy
committed
1 day ago
|

todo: give plans their own table and recover them after a crash
...
Todo already survived restarts inside the generic session_store KV row, but
the plan there was opaque JSON with no timestamp: after a server crash the
agent resumed a step marked in_progress with no way to tell whether it had
ever been done.
session_todos (user_id, session_id, tasks JSONB, revision, updated_at) now
holds the plan. Existing rows are migrated on first use and the KV source is
deleted in the same step, so clearing a plan is not undone by the next
process start.
Each step gains provenance fields (source, step_id, protocol_run_id) for the
protocol work, and insert_steps() splices a block after the step the agent
has already reached instead of making it re-issue the whole plan.
get_progress_message() now flags a plan whose in_progress step predates this
process once per (session, revision): re-check the real result before
marking it done.
The todo op "clear" now uses the same user scope as load/save; it used to
prefer ctx.user_id and could delete a different row than the one written.
Eugene Sukhodolskiy
committed
1 day ago
|
| 2026-11-04 |

export: token-served age snapshot of dialogue for an n-gram consumer
...
Navi had no way to hand its history to another machine. A database dump
was the obvious idea and the wrong one: prod's 597 MB is mostly things
that must never leave (Fernet-encrypted MCP keys, user rows, attachments),
and the dialogue an n-gram table wants is 14 MB of it.
So the export is a fixed, versioned SELECT — role, content, session, seq,
ts — over both message tables. Default corpus is user+assistant only:
tool output is 52 MB of scraped web text and command logs, and system
messages are near-duplicate boilerplate; either would dominate the counts.
A rolling window takes whole sessions, newest first, up to 150 MB, so the
snapshot stays bounded as history grows.
The snapshot is age-encrypted to a recipient whose private half never
reaches this server, so Navi cannot decrypt what it exports. The plaintext
gzip is deterministic, and its digest becomes content_id: an unchanged
corpus keeps the ciphertext and its ETag, so a consumer that polls every
few minutes downloads nothing until something actually changed.
Endpoints sit behind a bearer token (machine credential, not user auth):
/export/v1/manifest describes, /export/v1/messages streams and answers 304
on a matching If-None-Match. /health carries an informational freshness
object that never moves the aggregate status.
scripts/navi_export.py is the consumer side: keygen, then pull — manifest
first, download, checksum, decrypt, gzip-verify, atomic rename; exit 3
when nothing changed.
Eugene Sukhodolskiy
committed
4 days ago
|
| 2026-11-03 |

navi-web: make the batch shape the schema, drop Brave
...
Every recorded web_search and web_view call in production carried a scalar
(20 of 20, 8 of 8), including sessions where three searches on one topic went
out one after another. The recommendation was already written in three
places — the parameter description, the tool docstring and the server's
summary line — so prose was not the problem: `str | list[str]` renders as
`anyOf: [string, array]`, and the model took the first branch.
The parameter is now a pure array with minItems/maxItems and an examples
entry. A BeforeValidator folds a bare "query": "…" into a one-item list, so a
laxer caller still works; the cap rides in json_schema_extra rather than
pydantic's max_length, so four queries reach the code that runs three and
names the rest instead of failing validation.
Brave is gone on both paths: the keyed Search API (signup answers 403, so a
third backend that can never be enabled) and the ddgs engine of the same name,
which answers nothing from this host. The chain is SearXNG -> ddgs.
The persona's parallelism rule now separates "several items in one call" from
"several calls in one message" — it used to read "when the profile allows
parallel tool calls", which the model could only read as "not here".
Eugene Sukhodolskiy
committed
5 days ago
|

navi-web: pin the ddgs engines, drop bing, and give the fallback room
...
SearXNG answered the session's searches with noise. Bing, asked explicitly,
returns trending/homepage blocks for queries it cannot match (Block Blast for
a motherboard query); because the list is non-empty the chain accepted it as an
answer and never reached the working fallback. The request now pins
google,duckduckgo and asks for language=all — the pinned uk-UA was what made the
instance's DDG engine return CAPTCHA (kl=ua-uk) on every request.
The ddgs leg had a quieter bug. ddgs v9 has no "html" backend, and an unknown
name does not fail: it silently means "auto" — all engines shuffled together,
yandex and grokipedia included. That is where the slow, flaky, mixed-provenance
results came from. The engine set is now explicit and yandex-free. The 4s client
timeout and 12s chain budget also could not fit a real 1-13s multi-engine scrape,
so they become 10s and 20s.
Wire the Brave key placeholder the docs already described: optional, mapped
with a ":-" fallback so an empty value means off.
Eugene Sukhodolskiy
committed
5 days ago
|
subagents: a stopped run closes the tool it had opened
...
A tool card is drawn from tool_started and only its tool_call closes it.
When the user stopped a run while a sub-agent was mid-tool, the sub-agent
was cancelled, the parent dropped its sink, and the matching tool_call was
never sent — the card spun until the page was reloaded.
The sub-agent now reports its in-flight tool from the CancelledError
handler, and both batch paths (sequential and parallel) drain the sink
after cancelling instead of discarding it, so that event reaches the
client ahead of the parent's synthetic "stopped by the user" result.
The webclient settles anything still pending at stream_end,
stream_stopped and error as a safety net for a lost frame or a dying
server, with a visible "interrupted" note rather than a silent close.
The TUI needs nothing: it renders tool_call as a result panel and carries
no pending state, so it gets the fix through the backend alone.
Eugene Sukhodolskiy
committed
5 days ago
|
deploy: MCP servers' own venvs are synced by the deploy, not by memory
...
Each server under mcp-servers/ runs from its own .venv — the interpreter its
config in mcp_servers.d/*.json names — and Navi's venv does not carry those
dependencies. When a server's pyproject.toml gained one (trafilatura, Nov
2026) and the host was only pulled and restarted, the server died at import:
the manager could not connect, its tools silently left every profile's tool
list, and nothing reached the chat. SearXNG was healthy the whole time.
sync-mcp-venvs.sh installs every mcp-servers/*/pyproject.toml into its own
venv, stamped by a sha256 of the pyproject so an unchanged server is skipped
and a re-run is cheap. install.sh calls it as a numbered step; UPDATE.md
runs it unconditionally after a pull and verifies the MCP connection in the
journal; docs/mcp.md no longer reads as "install it yourself by hand".
Eugene Sukhodolskiy
committed
5 days ago
|
subagents: a worker persona, a live prompt fallback, and guards that scale
...
- assistant and designer_3d get their own subagent_system_prompt.txt; without
one a sub-agent ran on the orchestrator's prompt, or since ce55325 on nothing
but the scope anchor, and rarely finished the step it was given
- build the sub-agent system prompt in one place; the documented fallback to
profile.system_prompt had become dead code
- the thinking-stall guard killed healthy cloud reasoners at 12k chars after
13-35s; raise the char bound to 48k, keep the 60s wall, and make both settings
- a 300s run cap cut long ops work in half; raise it to 600s, configurable
Eugene Sukhodolskiy
committed
5 days ago
|
| 2026-11-02 |
webclient: a recall you can inspect, skip and cancel
...
Clicking the banner opens a dialog with the call's kind, when it was created,
a live countdown to the next fire, the repeat interval and the system message
the agent wrote for itself; skip and cancel live there too.
The cancel button in the banner and the sidebar never worked: ph-clock-slash
is not a Phosphor icon, so the glyph was empty and the button collapsed to
8x4px. It is ph-x-circle now, with a larger hit area.
/recall payloads carry created_at, and the countdown picks days or hours
instead of counting out "43230 min".
Eugene Sukhodolskiy
committed
6 days ago
|
sessions: the info dialog shows a pending recall
...
GET /sessions/{id}/info now carries the session's pending recall, and
GET /sessions/{id}/recall answers in one shape ({"recall": {...} | null})
instead of a flat object when found and a wrapper when not. The lookup is a
single indexed row rather than the newest 50 recalls filtered in Python.
Eugene Sukhodolskiy
committed
6 days ago
|
compression: keep the last four turns verbatim, not eight
...
Four turns still carry the current task word for word; everything older is
already summarised, so eight bought little and pushed the compression trigger
later than it needed to be.
The mid-turn window is unchanged: it counts messages, not turns, and
max(12, keep_recent*2) still floors at 12. navi_code keeps its own override of
12 turns.
Examples, docs and the two tests that assert the default follow.
Eugene Sukhodolskiy
committed
6 days ago
|
webclient: a dialog that answers which model is serving, and from where
...
The header only named a model when the route had fallen back, so "what is
running this session, on which host, with how much context" had no answer
in the UI. The context percentage doubles as the second way in: a session
bar is a fair place to ask what the bar measures.
GET /sessions/{id}/info reads in-process state only — the active route
entry, the health chain, the profile's effective window, run and client
counts — and the dialog polls it while open. /meta and the session head
now report the route's window instead of the global ollama_num_ctx, which
had been wrong for every cloud-backed profile.
Eugene Sukhodolskiy
committed
6 days ago
|
| 2026-11-01 |

llm: a key by any name, so a second cloud account needs no code
...
`_resolve_secret` knew exactly two variables: `OLLAMA_API_KEY` and
`EMBEDDING_OLLAMA_API_KEY`, because those were the only ones with a Settings
field to bridge to. Any other name — a second Ollama Cloud account on the
backup chain — was looked for in `os.environ` only, and pydantic-settings does
not export `.env` there, so the target silently received an empty key and
every call to it would 401.
The lookup now reads the `.env` file itself for the name it was given, in the
same CWD-relative way pydantic-settings resolves it, with `os.environ` first
and the two known Settings fields last. `python-dotenv` was already a
dependency; a malformed line in the file (production has one) does not stop
the variables below it from being found, and that is a test.
`llm.json` stays secret-free: each target keeps naming its variable, several
accounts are just several targets, and the docs spell out the chain.
Eugene Sukhodolskiy
committed
6 days ago
|

llm: routes that name a host, a backup and a window, and a profile that reads them
...
`llm.json` replaces `OLLAMA_BACKENDS_FILE`. It has targets (host and key
environment variable) and routes, where a route is an ordered chain of
`model @ target` entries, each with its own `context_window` and options. The
`default` route now starts on `glm-5.3-flash:cloud` and backs it with
`qwen3.6:35b-128k` on the home host, both at 131072 tokens, so a failover no
longer changes the budget the compressor and `num_ctx` are accounted against.
Embeddings get their own `embedding` route instead of a pinned host.
The engine classifies a failure (network, quota, auth, server, missing model),
cools that entry and moves the call down the chain; a background probe watches
only degraded targets and lets the next turn return to the primary once it is
healthy — it never interrupts a running turn. A profile joins a route with
`llm_route`, and the loader now actually reads it: `llm_route` and
`context_window` were declared on `AgentProfile` and written into every
`config.json` while the loader skipped both, the same silent no-op that
`is_admin_only` used to be, so every profile ran the legacy model list at the
global window. The writer emits them too, or an admin edit would erase the
route from disk.
The turn's resolved entry travels to the UI: the first chunk is stamped with
model and target, `model_info` carries `fallback` when a backup answered, and
the chat header shows a chip only then. `serving_primary` compares the target
as well as the model name, because a route may run one model on two hosts.
`/health` gains an `llm` block with the active entry, target states and missing
models.
`deploy/ollama/` keeps the local model's definition honest: MTP off (slower at
every context on this host), `presence_penalty 0` (the base model's 1.5 breaks
agentic tool calls), and `num_gpu 40` — Ollama's automatic fit claims a 128k
f16 slot fits and then dies mid-prefill with CUDA OOM; 40 of 42 layers leaves
~400 MiB and decodes at ~77 tok/s, 38 layers collapses to 48, and 39 OOMs.
q8_0 KV measured 37 tok/s against 70 on f16 here, so the cache stays f16.
Eugene Sukhodolskiy
committed
6 days ago
|
profiles: the Personal Secretary retires, and every reference goes with it
...
`secretary` had become a second general-purpose assistant: same models, same
tools, one temperature step apart from `assistant`, and admin-only for no
reason the user could see. The directory is gone and the eight remaining
profiles load clean.
A profile that no longer exists is not a harmless orphan — `Agent` resolves
`session.profile_id` on every run and raises `ProfileNotFound`, so any session
left behind would open and then fail on its first turn. Prod sessions were
moved to `assistant` first (115 rows, verified after), and the local clone
along with them (101).
The reaction fallback loses its role table: it existed because the admin
fallback was itself admin-only, which `assistant` never is. `spawn_agent`
falls back to `assistant` too, and the two prompts that recommended
`secretary` now name only `server_admin` and `developer`.
Eugene Sukhodolskiy
committed
6 days ago
|

webclient: a viewer for every content type, and a message sent mid-turn waits its course
...
The lightbox is now one dialog for every file the artifacts panel and the
content cards can show: audio and video players on the kit's own controls, a
text/source view with highlight.js, and a three.js viewer that replaced the
standalone stl.html frame with a model one. highlight.js emits .hljs-* classes
and ships no theme, so until now every highlighted block rendered in one flat
colour — app.scss now carries the palette.
While a turn runs, the send button no longer turns into Stop and vanishes the
draft with it: the message goes into the socket and is marked "queued" until
the stream_start of the turn that consumes it, the write-now-run-next model
navi-code uses. Send and Stop sit side by side, and the queue chip takes the
larger of the local and the server's counts.
The socket keeps its own liveness: a ping every 20 s, a watchdog that reopens a
socket silent for 45 s or stuck in CONNECTING, and a reconnect on resume from
background, network return or bfcache. On reopen it drops a half-written
message left by the dead socket, so the session_sync that follows is not hidden
behind a stream that will never finish.
Eugene Sukhodolskiy
committed
6 days ago
|

navi: a token count that matches the model, a type for every file, and a socket that admits it is dead
...
Compression decided everything in chars // 3, and on this workload that sits
~1.8x below reality — 101k real prompt tokens against a ~56k estimate — so the
trigger, the target, the truncate budgets and the pre-call size check all ran
in a unit half the size of the real one. TokenCalibration now fits
overhead + chars / chars_per_token per model from the prompt_eval_count that
every call already reports, and a profile declares its own context_window
(sent to Ollama as num_ctx) instead of every model being held to OLLAMA_NUM_CTX.
Superseded summaries were never demoted in the database: save() only updates
flags for rows in session.messages, a summary is is_display=False, so it lived
in session.context alone and every reload resurrected it — prod sessions
carried up to 15 live summaries. PgSessionStore.set_context_flag() writes the
flip for those rows directly.
Five extension tables disagreed about what a file is. navi/content_types.py is
the one detector — signature, then extension, then a text probe — it decides
the MIME the download route serves, and the session file list now carries the
type the viewers branch on.
The idle receive loop answers client pings and sends its own heartbeat. A
half-open socket keeps readyState at OPEN and never fires close, so traffic
coming back is the only proof the connection still works.
Eugene Sukhodolskiy
committed
6 days ago
|

navi-web: search backends, three-at-a-time batches, and pages read as markdown
...
web_search runs SearXNG first, DuckDuckGo with one retry after a real failure,
and Brave last; measured 0.2-1.4s through SearXNG against 1.7-11.8s on
scraping. Both tools take one item or up to three and run them concurrently —
three pages went from 17.3s to 1.9s, three searches from 3.5s to 0.6s — each
under its own deadline, so one hung backend or page is named in the output
instead of erasing what the others already returned. Text is capped at 20k for
one page and 40k for a batch. A call that opened nothing now raises: a green
journal line carrying "success": false is how a server whose browser could not
launch at all looked like a working one, and how 604 search calls in two weeks
reported no errors.
web_view returns the main content as markdown, not innerText after a tag
kill-list. The old path could not tell navigation from prose (462 of the asyncio
page's 1018 lines were shorter than 40 characters), collapsed tables into one
line per thousand characters — news.ycombinator.com came back as four lines —
marked no headings and dropped every link. trafilatura now picks the content by
density, and its html output keeps the whitespace inside <pre> that its markdown
output throws away, so the fragment is rendered here: # headings, fenced code
with its indentation, table rows joined by |, and up to 40 links lifted into a
"Links on this page" appendix rather than sprinkled through the sentences.
Threads are read again with comments when the extractor keeps less than a
quarter of the visible text; if it still finds nothing, the old visible-text
read answers and metadata.extractor says which one did.
trafilatura goes into the server's own venv, which deploy/install.sh does not
build — docs/mcp.md and deploy/UPDATE.md carry the install line. The tests load
the server by path, so it also joins Navi's dev extra.
Eugene Sukhodolskiy
committed
7 days ago
|

secrets: a placeholder may carry a fallback, and web search stops being a Settings field
...
An unset ${SEARXNG_URL} failed the whole navi-web connection, all three of its
tools and not only search, because resolve_secrets raises on any placeholder it
cannot fill. The rule is right for credentials, where a blank value quietly
becomes anonymous access, and wrong for a setting, where an empty SearXNG URL
costs one backend rather than the server. ${NAME:-fallback} (POSIX-ish, empty
fallback allowed) says which is which, and writing the fallback down makes it a
decision in the config instead of an accident of a variable nobody set.
Settings.brave_search_api_key and .searxng_url were read by nothing — the
navi-web server has its own config — so they go, with docs/config.md pointing at
mcp_servers.d/navi-web.json and deploy/env.template naming SEARXNG_URL where a
deploy actually sets it.
The literal-secret guard still holds: a placeholder name is a credential unless
it is in SETTINGS_VARIABLES, and a settings placeholder must sit in an env block.
Eugene Sukhodolskiy
committed
7 days ago
|
| 2026-10-12 |

designer_3d: the prompt gets rules for the tools it never named
...
The profile holds a shell, a code runner, file sharing, background tasks and the
session's own bookkeeping, and its prompt named none of them: terminal, code_exec,
share_file, tasks, notify, reflect, todo, plan, switch_profile, list_profiles,
schedule_recall, manage_recall, list_tools, get_current_datetime, gmail and
render_component all appeared zero times as tools — checked against config.json and
tools/enabled.json, not by eye.
A new section, "The rest of your toolbox", gives each one a rule. terminal and
code_exec stay in the profile, since the owner reports real use for them, so the
prompt has to say what they are for rather than leave them ungoverned: one working
directory and nothing outside the session, no connections to the local network
including from inside a script, install nothing and never a sudo, and the command
allowlist named as a filter on the first word rather than a cage — reaching a
forbidden command through bash -c, awk, sed, env, find -exec, a heredoc or a Python
script is the same act and forbidden for the same reason. A refusal is the boundary
working, not an obstacle to route around. They are also not a geometry tool: they
may not produce the final STL and may not stand in for lint/compile/render.
The rest get one line each — share_file only from the session directory, tasks as
the lifecycle of background sub-agents, notify for what is worth interrupting for,
memory and the recalls with secrets excluded, switch_profile as a move the user asks
for and not a way to widen one's reach, gmail as Navi's own mailbox the user is
addressed in, render_component as drawing in the chat and reaching nothing beyond it.
docs/profiles.md records the section, and that assistant's prompt covers code work
in its own "Code work" section instead.
Eugene Sukhodolskiy
committed
27 days ago
|

designer_3d: the sub-agent gets what the prompt promises it, and weather goes
...
The profile prompt tells a sub-agent it may use web search, page reading,
filesystem reads and image viewing, and that it must not write, publish,
share, run commands, update todos or call the modelling tools. The declared
sub-agent set said otherwise on both counts: it carried terminal, code_exec,
share_file, content_publish, todo and the whole navi-3d server, and it lacked
the image_view the prompt grants. It is now scratchpad, reflect, filesystem,
image_view, memory, list_tools, tool_manual plus navi-web (search, browse).
weather leaves designer_3d. It was declared in the profile and also merged
into every profile from tools/enabled.json, so both had to go; assistant and
secretary declare it themselves and keep it, and the admin-only profiles that
never asked for it lose it too. Since switch_profile unions the two restricted
profiles, this trims the 3D agent's schema rather than closing a door.
The agent's own set keeps terminal, code_exec, share_file and tasks — the
owner reports real use for them.
Eugene Sukhodolskiy
committed
27 days ago
|
navi-3d: a session id is a path segment, and it stops being a way out
...
The 3D server resolves paths itself, so the sandbox in areas.py never applies
to it: what Settings.resolve_path accepts, the tool opens, creates or
overwrites. It joined session_id onto session_files/ unvalidated, so
"../../../../.." resolved to "/" and every relative path was then "inside";
and it left ".." in a relative path unresolved before its containment check,
which is lexical — "../<other session>/x.scad" passed and only the OS
collapsed it at open time.
designer_3d grants navi-3d to the agent and to its subagents, so this was
host-wide read and write — compile_scad even creates the output's parent
directories — for an ordinary user, with the profile prompt as the only thing
in the way.
The id is validated as one segment now, and both branches resolve before
checking, which also catches a symlink leading out.
Eugene Sukhodolskiy
committed
27 days ago
|