Tools
The native tools agent-core gives every agent: execute, delegate, web tools, workflows, and the bounded connection control plane.
agent-core ships its native tools as tools/*.json schemas under the
schema-first tool contract. They are loaded
through the same discovery path as any package's tools, validated by AJV at
load time, and dispatched through the same policy, feature-gate, and approval
machinery — being built in grants no shortcuts.
The model receives every visible tool’s root description and full JSON schema on every step. These fields are operating instructions, not UI filler: the root description should make selection, recovery, and safety boundaries clear, while property descriptions explain only input-specific semantics. Avoid copying long procedures that belong in skills or rules.
| Tool | Purpose | Risk profile |
|---|---|---|
execute | Run a command on the app, an attached host or an executable source, or activate a skill; background: true detaches a long-running shell that reports back by itself when it ends | Destructive, open-world |
delegate | Spawn a subagent with its own full agentic loop | Destructive, open-world |
workflow | Create and manage scheduled workflows | Write, feature-gated |
web_search | Search the public web | Read-only, network |
web_fetch | Fetch, render and optionally store a URL's content | Network; storing needs drive.write |
ask_question | Pause and ask the user explicit questions | Read-only, interactive |
connect | List, add, check, authorize or disconnect MCP, channel, git, provider and source connections through named secret-free projections; human secret/OAuth steps stay on secure surfaces | Write/open-world, per-kind feature-gated |
todos | Maintain the durable goal, loop and actionable task list | Local state only |
compact_history | Trim earlier context in place to free the window (remove/summarize/truncate by tool_use_id) | Write, non-destructive + reversible (never forks or rewrites history) |
Per-agent tool selection
Which of these tools (plus every package-contributed tool) a given agent
actually sees is configurable per agent in the chat config view's Tool
Access panel. The selection is a convenience narrowing: a deselected tool
is absent from the model's function list and rejected on a hallucinated call —
in chat, delegate subagents, and workflow runs alike — but the selection can
never widen past the package enable/disable state or the caller's feature
grants. A single tool can also be switched off without leaving the default
all mode (written through the API or the manage-agents skill — the panel
leaves all mode when you untick), and a workflow can narrow further for its
own runs (workflow's tools_off); neither can widen. Full semantics (default-on, frozen custom allowlist, agent-wide save):
the Tool Access section.
Slow calls
Any tool call — native or package-contributed — that takes at least
perfSlowToolMs (default 30 s, live) writes one perf.slow warning with
the tool's name, its duration and whether it failed, never its arguments or
output. It reaches container stdout and the agent's run log. A call cut short by
a Stop is not reported: its duration is the Stop's, not the tool's.
execute
execute is the canonical execution substrate, with two action modes.
action: "shell"
For app/host execution, runs bash — pipes, redirects, chaining, git, python and installed binaries — with three independent guards:
- Command analysis. The command is parsed into an AST (not regex-matched):
destructive system binaries (
mkfs,shutdown,sudo, …),evalas a command, fork bombs, dangerousrm -rfforms, and references to the platform app zone or credential files are rejected outright. Quoting and backslash escapes cannot hide a name — the parse resolves them the way bash does. A nestedbash -c "…"is re-analysed rather than rejected, by this guard and by the URI-policy gate alike: a blocked binary inside the nested script is caught and its paths are checked exactly as if the command had been written flat, while ordinary nesting keeps working — up to four levels, past which the command is refused. The glued spellings (-lc,-cl,-ec) are the same flag, analysed the same way. A wrapper prefix is resolved to the command it actually runs, sotimeout 5 …orenv A=1 …cannot hide a blocked binary. This layer is defence in depth; the kernel sandbox described in security is the boundary — except in the operator's unconfined host mode, where these checks and the URI-policy gate are the only enforcement. - URI-policy gate. The working directory and every path the command
touches are evaluated against the project's
URI policies. The
cwdmust fall under a source whose policy grants source-levelexec; path arguments needread— orwritefor mutating commands and redirect targets. Uncovered paths deny by default. See security for the full rules. - Environment sanitization. The child process env is stripped of secret-like variables before spawn; only credentials a skill explicitly declared are allowed through.
Key inputs: command (required), cwd (a source URI like data://notes, an
absolute path, a path relative to the project data root, or ${SKILL_DIR}
when a skill is active — defaults to the project data root), skill (with
several skills active, names the active skill whose ${SKILL_DIR}-family
substitutions bind for this call — default is the most recent activation),
timeout (app/host default 120 s, explicit max 300 s), env, and a one-sentence reason shown
to the user in the result card.
Stop behavior depends on the target
The timeout is a deadline, not the only way a command ends. On the app plane, stopping a run
terminates the command’s whole process group — a polite signal first, then a
forced kill after a short grace — so a sleep, a build step or a curl started
by the command dies with it instead of outliving the conversation. The result
keeps the output captured up to that moment and the card is labelled stopped.
A command running on the host plane is the exception: the host broker cannot
cancel a command in flight, so stopping the run only stops waiting for it — the
command finishes on the host under its own timeout, and the result says so.
For connector execution, Stop also ends the wait without guaranteeing that the remote
process terminates; inspect state before retrying.
A background shell is deliberately outside this: it is meant to outlive the
turn, so stopping the run leaves it alone. End one from its card's Kill control
or the stop.sh script, which terminates its process group the same way.
Container or host
For non-connector execution, execute runs inside the application container unless the working directory
resolves into a host-plane source — in which case the command is handed to
the operator-provisioned host broker and runs on the host machine instead. The
plane is decided by the owning source's connector, never by a name: an ordinary
in-container source called host-notes stays in the container. A bare absolute
path resolves against the container first and only falls through to the host
when no container source owns it, so an installation with no host sources
attached behaves exactly as before.
Reaching the host also requires the caller to hold exec.host (no grant
below the admin tier — enumerated for admin, '*' for owner; grantable to
any custom role) and the operator to have provisioned the broker. This is
not a privilege escape hatch: no role can bypass the host's OS confinement (the
one relaxation is the operator's own ceiling file, which no role can select),
host commands are additionally clamped to an operator-owned allowlist, and the result reports the
effective roots that were applied together with the confinement the command
ran under. background: true works on the host plane when the operator ceiling
names a lifetime limit for detached runs — the job survives an application
rebuild and reports back afterwards; without that limit the denial says so. It
works on an executable source whose provider supports detached runs too, under
that provider's own ceilings. The
full model is on the security page.
Executable sources, including Webtop
An explicit cwd: "<source>:///config" selects the exact registered source connector.
The slug may be renamed or user/agent scoped; it is not a machine type. No match or a denied
provider never causes execution on another target. Local and host sources retain their
existing path gates; other connectors must advertise and implement authenticated execution.
Every exec-capable connector names its own ENTRY feature in its manifest, and the platform — not the connector — checks it: a caller must hold every id the kind declared before the provider is reached. After that the SOURCE's own path policy decides whether that caller may start a command in that working directory, exactly as it does in the container. Both refusals are explicit, so a denied call says which one it was.
For Webtop, <source>:// means /; its declared entry is exec.machine, and the provider then
checks accessible source scope and the existing machine ownership/lifecycle authority;
auto/stopped sources can start. The app/host path sandbox does not apply inside Webtop — the
container is its own boundary, and the policy above governs where a command may START, not
which paths it may touch; filesystem-tool policy remains separate.
Webtop takes the caller's own env and runs background: true when the machine's desktop image
supports them, and announces the gap with its remedy when it does not; it receives no platform
ticket, active-skill credentials or substitution environment. Omitted timeout keeps the sidecar's
30 s default; explicit 1000–600000 ms values are clamped by the container's configured ceiling.
Authenticated external MCP callers can use this connector path without a stream. App/host
shell, skill activation and calls with no connector URI still require an active stream.
The removed machine_exec name has no alias; custom selections need deliberate updates,
since enabling execute also exposes its other modes.
Background shells
execute({ command, background: true }) detaches a long-running command (a
build, a test run, a watch, a server) and returns an opaque shellId plus two
durable URIs — statusUri and outputUri — immediately instead of blocking.
The process keeps running across turns under the same three guards above —
background adds no bypass.
The result comes back by itself. When the shell exits (or is stopped, or is
lost to a platform restart), the platform continues the conversation with a
completion report: its own result card carrying the exit code and a bounded
head + tail of the output. The report is a background_report — a call the
platform writes and the model cannot make, so an agent reading it cannot mistake
it for something it ran itself and run the command again; while the shell runs,
the agent's prompt lists it as still running and says the report comes in a
later turn. The agent is told to dispatch the shell and end its turn — there is
nothing to poll and no tool to poll with.
The report turn is a live turn. When the person who started the shell has
that conversation open, their own browser continues it — a normal streaming turn
they can watch token by token, stop, and answer approvals in. Only when nobody is
there (the tab is closed, or the conversation is bound to a messaging channel)
does the platform run the turn on the server. The continuation runs as the
person who started the shell, with their rights re-resolved at that moment,
never while an approval is open on that conversation, and never over a turn
that is already in flight — a report that arrives during a long turn is
delivered the moment that turn ends, not at the next message. The switch is the
shellBackgroundNotify platform setting — off, the shell still ends durably
(its status and log stay readable under the conversation) and simply is not
announced; the report's output budget is shellReportTailChars, and the grace
the browser gets before the server takes over is backgroundWakeClientGraceMs.
While it runs, the result card and the Shell tab show the live output — the
browser polls a conversation route, so watching costs no model tokens — and the
full log at outputUri (one JSON row per captured chunk) is readable at any
time with fs_read. Reading more and stopping are routes, not model tools:
the card's Kill control, and the execute-skill's shells.sh / output.sh /
stop.sh scripts, which authenticate with the per-stream session ticket and
reach only shells the caller started. Keeping read and kill off the tool plane
is what lets the one execute tool keep its per-tool approval floor.
Long jobs belong in background: true, never in a hand-rolled nohup … &: a
nohup'd process has no record, no report, no Kill control, and dies silently on
a restart.
You can only reach shells you started, and only the agent that started a shell
can stop it — the same person can read a shell's output from another agent's
chat, but the Kill control there answers "not yours to stop". A detached process
cannot re-authenticate to the platform after the turn that launched it ends. The
chat Shell tab mirrors all running/finished background shells with a live tail
and a Kill button. A background subagent's own background shells report back to
that subagent (they live in its run folder), and a foreground subagent runs
background: true in the foreground and says so.
Agents messaging each other
Any agent — or a subagent — can queue a message into a conversation it may
write: another agent's conversation, a workflow run's transcript, or a
background subagent run. The manage-conversations skill's queue.sh does it
(POST /conversations/:id/inbox); the receiving conversation gets the message
as a report card naming the sender, and continues by itself — as the SENDER's
user, exactly as if they had typed in that conversation (never as the
conversation's owner): live in the sender's open browser when they have it open,
on the server otherwise. Replies use the same
script, and the platform keeps the exchange bounded: an agent-to-agent hop
ceiling, a cap on undelivered messages per conversation, and a minimum interval
between two messages to the same target (all platform settings). The message
text reaches the receiving agent inside the untrusted-content envelope — it is
data, never an instruction — and the sender is always the caller's own session.
Live output vs. the result the model reads
These are two independent channels, and only the second one reaches the model. Live output is a App surface: it streams to your card as the command runs, paced and capped so a chatty command cannot flood the browser. Once the command finishes, the card switches to the captured result — the same value the model receives, and subject to the same cap.
Both channels are bounded. The limits that govern how much output is produced, captured and handed to the model are admin settings: Live Shell Flush Interval (the minimum gap between two live frames — output produced inside the gap is buffered, which is what keeps a firehose from overwhelming the browser), Live Shell Frame Max and Live Shell Stream Cap, the captured output per run (Shell Max Output), the per-background-shell ring (Background Shell Ring), and Tool Result Content Cap — the last of which applies to every tool, so it is usually the limit you actually hit. How much of that the card paints is a fixed rendering window on top.
Whenever a limit trims something, the card says so. For shell output the card keeps the most recent lines, so a very long run shows its newest output rather than its first — the same reason the card scrolls itself to the bottom while a command runs.
action: "skill"
Activates a SKILL.md bundle by name: the skill body is rendered into
context with ${SKILL_DIR}, ${SKILL_URI}, ${SESSION_ID}, and
${NEURALIS_API} substitutions, and the skill's declared credentials are
resolved and staged for subsequent app/host shell calls — the model never sees the
secret values. For connector-backed skills, use the actual directory URI from activation and a relative
script command; no ticket or credential environment is projected there. App/host skill scripts run with
cwd: "${SKILL_DIR}", so imported agentskills.io
bundles written as bash scripts/foo.sh work without rewrites. The full
activation flow is documented in
package-system skills.
delegate
delegate spawns a child agent runtime with its own complete agentic loop
and returns its final summary to the parent. Key inputs:
task(required) and optionalcontext, placed ahead of the task in the child's first message; optionalagentandmodeloverrides.tools/blocked_tools— allow/deny lists over the parent's tool catalog, narrow-only and enforced when the child CALLS, not only in what it is shown. An emptytools: []is rejected by schema — omit the field to inherit the full set (a child with zero tools cannot work); an emptyblocked_tools: []blocks nothing and is read as if the field were absent. Every name intoolsis answered: the ones that survive go to the child, the ones that do not are named back in the result — not in the subagent's own catalog, barred from every subagent, outside the chosen agent's ownallowed-toolsceiling, or removed by the call's ownblocked_tools— and a list of which nothing survives is refused instead of starting a tool-less child. A name that is not in the child's catalog is reported the same way whether it is hidden from the caller or simply does not exist.disabled_packages— package denylist for the run; a disabled package loses its tools, its injected rules/instructions, and its packages-overview detail. The list unions with the parent's disabled set — a subagent can only narrow, never re-enable.timeout_ms(300 s–1680 s, default 1680 s) — a soft timeout: when it fires, the child gets one final text-only wrap-up turn and returns a focused partial summary instead of dying empty-handed.max_steps(default 200, max 200) triggers the same wrap-up at the last step. The ceiling sits inside the band where the wrap-up turn is provably still delivered: past that band the tool-call wrapper would fire before the child and discard the answer, so a larger value would be reduced rather than honoured. If even the wrap-up turn does not land within a further 90 s, the run is cut off — and a cut is reported as partial with the work it did, never as a failure. Because those runs never got to write a conclusion, the answer the parent receives opens with a line saying it is partial, so a half-finished result cannot read as a delivered one.
The child's reply to the parent is its last substantive turn, not every turn concatenated — a step that produced no text does not erase the previous one. If that answer is longer than the platform's delegate answer cap it is clipped keeping both ends, with a marker naming how much was dropped and where the full transcript lives; a conclusion sits at the end, so head-only truncation would throw away the part that was asked for.
Every delegate run is persisted under the parent conversation —
meta.json plus a full messages.jsonl child transcript — so finished and
failed runs alike can be opened read-only from the chat UI's Delegates tab.
Background runs
background: true detaches the subagent from the turn that started it. The call
returns immediately with a run id and two URIs — statusUri and transcriptUri —
instead of an answer, and the subagent keeps working while the agent replies to
you.
Detaching is a top-level capability. A subagent may still delegate, but its own
child runs in the foreground and is awaited, so exactly one report reaches the
conversation instead of a grandchild surfacing in a timeline that never dispatched
it. The agent is told this in the tool result rather than being silently
downgraded. The model watches it with the same fs_read it uses for any other file: the
status file says whether it is still running, and the transcript grows as it
works, so a mid-run read is meaningful rather than empty.
When the run finishes, the conversation picks it back up on its own — the answer arrives as its own result card in the timeline and the agent continues the work it was for, without you having to prompt again. The card is clearly a report, not a message you sent and not a call the agent made: the platform wrote it when the run finished. Because it is a tool result rather than a typed message, it does not open a new session either — however many background runs report into a conversation, it stays one continuous thread. That continuation runs as the person who started the run, with their permissions re-resolved at that moment, and it waits its turn: it never interrupts a response in flight and never fires while an approval is open. When it has to wait, the report is queued, not dropped — it rides in on whatever turn happens next in that conversation, so a run that finishes while you are busy still reports back rather than going quiet. If an administrator turns the report-back off, the answer is still in the transcript and the Delegates tab.
Nothing new appears in the model's tool list for any of this. Observation is a file
read, and the two rare mutating operations — stopping a run, or continuing a finished
one with a further instruction — are HTTP endpoints the delegate-skill reaches
through its own scripts. One delegation tool is still the whole surface.
A background run keeps the permission profile that was in force when it started — and if you switch the conversation to a stricter mode meanwhile, the run adopts that stricter one the next time it continues. It can only ever be asked to confirm more, never less. Whatever the call narrowed — tools, packages — stays narrowed for the run's whole life, including when it is continued later: a run can lose reach if its owner's permissions shrink, and can never gain any. If it stops for an approval it parks and holds nothing open; you answer in the Delegates tab, and it continues from the same transcript on its own — the approval appears there while you are looking, without a reload, and it survives anything else you do in the conversation meanwhile. Only the person who started the run can see and answer it.
One thing to expect under safe or balanced: because the guard asks per tool, the dispatch itself needs your approval first, so a background run does not begin until you answer that one prompt. After it, the run is genuinely detached.
Stopping a run reports too. A run you cancel comes back as a stopped card rather than a silent one — it says what it managed to do before you ended it, and it never wears the finished tick. A run that ends badly says why: the reason leads its report instead of the generic "ran no steps" line, and a run that never completed a single model turn — most often a model that is not available to the project — is reported as failed rather than quietly succeeding with nothing to show. Whichever way a run ends, it leaves nothing open behind it: any question or approval it was still waiting on is closed at the same moment, so no card outlives the run that raised it.
A platform restart ends a running background subagent as partial, keeping the transcript up to the cut; a run parked on an approval survives the restart and is still answerable afterwards. How many background runs may be in flight at once is a platform setting.
A delegate child inherits the conversation's permission profile (safe / balanced / auto) — it is not a tool input, so the model cannot lower its own guard. Under safe or balanced, when one of the child's inner tools needs approval the child pauses mid-run: the delegate card morphs into that inner tool's own card with an Approve / Deny footer, and on your decision the child resumes exactly where it left off (deny lets the child continue with the refusal noted) — durable across a tab reload, and isolated per delegate when several run in parallel. Under auto the child never pauses.
workflow
Creates, changes, runs and reads the project's workflows. Its keys are four
verbs, applied in the order set → run → get → list. set without a
workflow_id creates a workflow (a title and an instruction are required; it
is active unless you ask for a draft); with one, it changes only the fields you
send — the whole setup: the schedule (cron, one-shot, or none for manual-only),
the first webhook and its body contract, the model, the goal and loop, the
packages switched off, the tools switched off (tools_off — narrow-only names
from the <packages> block; the list replaces the stored one and [] clears
it), the runtime limit, status, and delivery to bound channels. run starts a run, get returns a workflow's whole setup (or one
run), and list lists them. Every verb of a call is checked before anything is
written, so a call you may not make changes nothing; when one verb succeeds and
another is refused, the call succeeds and names the refusal. The rarer
operations — templates, archive / restore / permanent delete, channel bindings,
run cancellation, extra webhooks, revoking a key — are the manage-workflows
skill's scripts over the same routes and permissions. The tool never issues a
key: a secret in a tool result would stay in the conversation. Each call renders
as a workflow card in the chat.
A per-run wall-clock limit (max_runtime_minutes) is one of two independent
execution bounds — the agent's step budget limits a run too, and the run's
prompt shows it live as a Steps counter in the session block; the wall-clock
platform ceiling defaults to 8 hours and is administrator-configurable. Work
that must run for hours needs both raised. run and get with a run_id
accept a bounded wait_seconds (clamped to an administrator-set cap, default
120 seconds, at most 300) and return when the run ends — a wait that expires
with the run still queued or running returns the snapshot, not an error. The
tool requires workflow.read and escalates to workflow.write (set) and
workflow.dispatch (run). See workflows.
Web tools
Both are gated by the core.web feature and share a hardened fetch pipeline:
DNS-pinned requests, private-IP and redirect validation, and per-call host
allow-lists, so the model cannot reach internal addresses.
web_search— a provider broker with five strategies:auto(the default — walks the order an administrator configures and fuses results when several providers are available),native(the search built into the model you are running on), andtavily/brave/searxngfor one specific provider. Every strategy falls back to a keyless engine if it comes up empty, so a named provider is a preference rather than a requirement, and a provider with no key configured is skipped rather than failed. When the keyless engine does answer, the result says why — which provider dropped out and whether it was unconfigured, failing, or simply not available on your model — so a weaker answer is never silently unexplained. Every result also carries its publication date, orunknownwhere none could be determined — on some providers that date is when the page was last updated rather than first published, so read it as the newest date the provider will vouch for. Anything the search had to warn about — a page it could not read, a step it had to skip — is written into the result the model reads rather than kept to one side. When a domain filter removes every result, the answer says so and names the filter, instead of blaming whichever provider ran last. Native search runs on the conversation's own model and is metered like any other model use — its tokens and the vendor's per-search fee both count towards your usage and spend limits. If the model you are running is not one whose vendor offers native search, the strategy is skipped rather than quietly redirected to some other vendor's model.time_range(day,week,monthoryear) asks for recent results only; each provider is given the equivalent filter in its own dialect, and one that has no recency filter — or cannot express that exact window — runs the search anyway and says so in the result rather than quietly returning older pages.allow_domainsandblock_domainstake bare host names, which are handed to the providers that accept a domain filter so they spend their result slots inside your policy, and are enforced again on the way back whether or not a provider honoured them.mode: "research"adds query-variant fan-out, freshness/authority ranking, and optional full-page Markdown for top results; the follow-up variants run on a cheaper lane with an administrator-set cap on how many may use a paid provider, so a research call costs a bounded number of paid requests rather than one per variant, and the result says how they were split.web_fetch— fetches one URL and returns its readable content, up tomax_chars. Handles HTML, Markdown, JSON, XML, plain text and PDF, falling through progressively simpler extractors for hostile markup; servers that negotiatetext/markdownskip extraction entirely. A PDF gets its own, administrator-set size ceiling rather than the text-derived one — above it the call fails with a message that names the limit, because a PDF cut in half yields no text at all and reads as a corrupt file.formatchooses the rendering (markdown,text,raw,json) —rawreturns the unprocessed body for pages the extractor gets wrong, andjsonparses the body whatever content type the server declared, saying so in the result header when it is not JSON. A page whose content only appears once a browser runs it is reported as such instead of returning its chrome silently.timeout_msbounds the network phase — DNS, redirects and the body read — while extraction runs after it.save_tostores the result (tmp, or adata://path in the project drive) and additionally requires thedrive.writefeature; without it the page is still fetched, just not stored.
Interaction and context tools
ask_question— asks the user one or more explicit questions (with optional options and multi-select) and blocks the stream until answered. Use it when a decision genuinely belongs to the user. Subagents never receive it, at any depth: a subagent has no path to a person, so a question asked there would wait for an answer that could never arrive. A task that needs a decision belongs in the agent you are talking to, not in a delegation.todos— three verb keys: withoutclosea call appliesplan, thenmark; withclose,markandcloseend the current cycle first andplanopens the next, so one call replaces the agent's own goal.planformalizes one locked outcome per cycle with immutable acceptance conditions, sets the loop bound and adds executable todo steps — in one call if needed;markupdates or removes steps and marks satisfied conditions by the number the prompt lists them under (or their exact text, all-or-nothing) without resending the goal. Meeting the last condition achieves and archives the goal together with its todo list as a versioned entry in the conversation'smeta.json(sessionLedger) and empties both, so the next phase starts with a freshplan;close: "achieved"closes a fully-met or condition-less goal explicitly,close: "cleared"drops an obsolete goal the agent authored itself (or, with no goal open, its own todo list), and a goal the user or a workflow set stays locked to the agent ([set by … — locked]in the prompt; a workflow template withevolve: "agent"lifts the lock and the marker drops the suffix). A todo list with no goal archives itself when every step is completed. The current state — and one line naming the closed cycles — is always visible to the model in the system prompt.compact_history— frees context-window space with an in-place, reversible, non-destructive trim when unusually large low-value output or a completed phase competes with the active goal (remove, summarize, or truncate). It is not a continuous or fixed-threshold ritual. It references a tool output by itstool_use_idhandle (or a whole non-tool row by sequence number) from the conversation map. The conversation is never forked or rewritten — the trim is re-applied to the model's prompt each turn and a reader can toggle it off ("Show full"), so it is fully reversible (destructiveHintisfalse). It never touches the system prompt or thecompact_historyrows themselves. Each call shows in the timeline as its own card: what was trimmed and how, how much it saved, whether it is currently applied, and — for a whole-conversation summary instead —/summarize.
connect
connect is the common model-facing control plane for MCP servers, channels,
git remotes, provider credentials and brain sources. A call may combine
remove, add, check, authorize and list; the handler validates every
requested input and required feature before any write, then applies that fixed
order. A later runtime refusal preserves completed operations and reports the
refusal rather than promising transactional rollback.
Each kind keeps its own grant (mcp.connect, channels.connect, git.connect,
credentials.self, or the source read/mount grants), scope rules and
authoritative package route.
list answers per KIND: a kind the caller may not reach is named as its own
refusal and the kinds that answered still return their rows, so one refused
lane never empties the result.
Results are named projections rendered as the answer TEXT the model reads and
as the card's data: connection state, exact-user hasValue, public channel
descriptors with their required field names, or a human handoff. Secret values,
creator ids, source paths and raw config never enter the model result. An OAuth URL is opaque and
must be opened by the human; the model never follows it. Channel removal is a
disconnect, source removal points to the governed source workflow, and git
checks report local configuration rather than claiming vendor health.
Inbound channel senders are still identified by pairing: an unknown sender gets an expiring code that the connection owner approves in the Channels panel.
Multimodal tool results
Tools that return images, audio, or other media — a filesystem read of an
image, a virtual-desktop screenshot — surface those as real content blocks to
a model whose capabilities accept the matching input. The media is also
persisted with the conversation, so it survives a tab reload or a resumed
turn: the agent sees the same pixels it saw live, not a flattened text
summary. Where the media came from a file with a stable address, only a
reference is stored and the bytes are re-read (under your own access) on
reload — a source you have since lost access to, or that was deleted, degrades
to a short "no longer available" marker rather than breaking the conversation.
Server-rendered media (for example a PDF rendered to an image) is kept inline,
since re-rendering it would not be reproducible. Attachments you add with a
⟦fs:read:…⟧ reference follow the same rule: an image/audio attachment is
inlined as real pixels for a capable model, and a metadata marker otherwise.