@agent-core

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.

ToolPurposeRisk profile
executeRun 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 endsDestructive, open-world
delegateSpawn a subagent with its own full agentic loopDestructive, open-world
workflowCreate and manage scheduled workflowsWrite, feature-gated
web_searchSearch the public webRead-only, network
web_fetchFetch, render and optionally store a URL's contentNetwork; storing needs drive.write
ask_questionPause and ask the user explicit questionsRead-only, interactive
connectList, add, check, authorize or disconnect MCP, channel, git, provider and source connections through named secret-free projections; human secret/OAuth steps stay on secure surfacesWrite/open-world, per-kind feature-gated
todosMaintain the durable goal, loop and actionable task listLocal state only
compact_historyTrim 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:

  1. Command analysis. The command is parsed into an AST (not regex-matched): destructive system binaries (mkfs, shutdown, sudo, …), eval as a command, fork bombs, dangerous rm -rf forms, 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 nested bash -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, so timeout 5 … or env 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.
  2. URI-policy gate. The working directory and every path the command touches are evaluated against the project's URI policies. The cwd must fall under a source whose policy grants source-level exec; path arguments need read — or write for mutating commands and redirect targets. Uncovered paths deny by default. See security for the full rules.
  3. 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 optional context, placed ahead of the task in the child's first message; optional agent and model overrides.
  • 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 empty tools: [] is rejected by schema — omit the field to inherit the full set (a child with zero tools cannot work); an empty blocked_tools: [] blocks nothing and is read as if the field were absent. Every name in tools is 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 own allowed-tools ceiling, or removed by the call's own blocked_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), and tavily / brave / searxng for 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, or unknown where 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, month or year) 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_domains and block_domains take 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 to max_chars. Handles HTML, Markdown, JSON, XML, plain text and PDF, falling through progressively simpler extractors for hostile markup; servers that negotiate text/markdown skip 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. format chooses the rendering (markdown, text, raw, json) — raw returns the unprocessed body for pages the extractor gets wrong, and json parses 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_ms bounds the network phase — DNS, redirects and the body read — while extraction runs after it. save_to stores the result (tmp, or a data:// path in the project drive) and additionally requires the drive.write feature; 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: without close a call applies plan, then mark; with close, mark and close end the current cycle first and plan opens the next, so one call replaces the agent's own goal. plan formalizes one locked outcome per cycle with immutable acceptance conditions, sets the loop bound and adds executable todo steps — in one call if needed; mark updates 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's meta.json (sessionLedger) and empties both, so the next phase starts with a fresh plan; 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 with evolve: "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 its tool_use_id handle (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 (destructiveHint is false). It never touches the system prompt or the compact_history rows 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.

On this page