SDK tools, workspaces, and approvals
Tool contract and trust origin
Tool is an object-safe, Send + Sync extension point. Each implementation returns a stable ToolSpec and an explicit Send future. Duplicate names are rejected when the runtime is built.
A tool receives a ToolInvocation and a ToolContext containing cancellation, bounded progress, optional workspace, policy, approval, and host-input access. It returns ToolOutput or ToolError. Failures normally become failed tool results sent back to the model, while hosts observe a typed ToolFinished result.
Tool::security distinguishes two trust models:
ToolOrigin::HostProvidedis the default. It is trusted in-process host code, and SDK policy cannot sandbox it if it ignoresToolContext.ToolOrigin::BuiltIndeclares the read, write, process, network, skill, or instruction-discovery classes the adapter enforces.
DiagnosticsSnapshot::tools exposes the origin and declared classes. In particular, a host can distinguish a network-capable built-in from a host-provided tool. A declaration is inspectable policy metadata, not an operating-system sandbox.
The SDK does not validate JSON against a tool schema before invocation. Implementations must deserialize hostile model output, reject unknown or ambiguous inputs, and cap resource use.
Preparation and parallel execution
Tool::prepare validates and resolves an invocation once, before scheduling. Preparations within one model batch run concurrently and their outputs return to the scheduler in model order. The default implementation wraps Tool::call with ToolExecutionPolicy::Exclusive, so existing and custom tools stay source-compatible and run alone. Tools must opt in before the runtime overlaps them.
A resource-aware implementation should keep prepare as its canonical path and delegate its compatibility call method to tool::call_prepared. This avoids maintaining separate parsing, authorization, and execution flows. The helper prepares the invocation, authorizes its declared capabilities, and runs its one-use executor.
A resource-aware tool returns PreparedToolInvocation::resource_aware with:
- every structured
CapabilityRequestneeded by the invocation; - a
ToolResourceAccesslist; - start metadata; and
- a one-use executor that owns the parsed arguments and resolved state.
The coordinator authorizes the declared requests before consuming an execution slot. The executor receives AuthorizedToolContext, which has cancellation, progress, host input, and workspace access, but no authorization method. A tool that cannot declare all authority before execution must remain exclusive and use ToolContext.
Each access is Shared or Exclusive. Shared access can overlap shared access to the same resource. Any exclusive access conflicts with shared or exclusive access to an overlapping resource. Resource kinds cover canonical workspace paths, directory trees and membership, managed process IDs, session or manager state, response-store IDs, and namespaced opaque keys. Opaque keys must use a stable owner namespace. Resource values are omitted from Debug output.
Filesystem tools must build resources from the same ResolvedWorkspacePath retained for execution. Resolve with resolve_for_read or resolve_for_write, declare access to its canonical path and any directory scope, then call Workspace::revalidate on that object just before I/O. Do not turn it back into an unchecked argument string or resolve it a second time.
RhoBuilder::max_parallel_tools sets a required nonzero limit and defaults to one. The limit bounds active execution, not approval waits. Independent eligible calls can run up to the limit. Conflicting calls run in model order, and an exclusive call forms a model-order barrier. Results still enter provider and persisted history in model order.
Implementors that opt in must:
- parse hostile arguments during preparation;
- retain all resolved security facts and tool-owned state in the prepared executor;
- declare every capability and scheduler resource the executor will use;
- revalidate retained filesystem facts immediately before I/O;
- cooperate with cancellation and bounded progress or host-input queues; and
- avoid work that outlives the invocation unless the tool remains exclusive and documents that lifetime.
Presentation and progress
ToolMetadata carries operation kind, paths, command summary, URLs, and unified diffs. ToolProgress adds a message and optional units. These are presentation values, not authorization decisions or safe audit values. Do not infer authority from display strings or log tool arguments and output without host redaction.
Progress uses a bounded channel and applies backpressure. Tools should stop cosmetic progress when the receiver is dropped and always give cancellation priority.
Independent capability defaults
Registering a tool exposes its schema but grants no sensitive authority. The independently evaluated classes are:
| Capability | Default |
|---|---|
| Read a path | Denied |
| Write a path | Denied |
| Execute a process | Denied |
| Network access | Denied |
| Load a skill | Denied |
| Discover workspace instructions | Denied |
| Host approval | Denied when no handler is configured |
| Workspace root | Absent |
ScopedWorkspacePolicy opts into each class separately. Network URL hosts and tool-managed network built-ins are separate grants. Paths outside the primary workspace additionally require allow_outside_workspace_paths, whether they come from an attached root or unrestricted path resolution.
Each CapabilityRequest contains a CapabilityOperation and CapabilitySource. The source distinguishes a host-provided tool, built-in tool, and prompt construction. Policy and approval code receives owned structured facts and must never parse a display command, shell preview, or model explanation.
Workspace path rules
Workspace::new requires an absolute native path to an existing directory, rejects parent traversal, verifies it is a directory, and stores its canonical form. Native NUL units are rejected. Windows prefixes are interpreted only on Windows; Unix backslashes and colon characters remain ordinary filename characters.
Relative paths resolve under the primary root. By default, absolute paths are accepted only when they are component-wise under the primary root or a root deliberately attached with Workspace::with_granted_root. Hosts that need process-wide path resolution can opt in with Workspace::with_unrestricted_file_access; this also permits parent components in requested paths. A path under an attached root is labeled PathScope::GrantedRoot; a path accepted by the broad mode is labeled PathScope::UnrestrictedFilesystem. Both scopes remain distinct from the primary workspace for policy checks. The unrestricted path-resolution mode does not grant read or write authority by itself.
Use the operation-specific APIs:
resolve_for_readrequires an existing target, follows symlinks, returns the canonical target, and rejects any target outside the workspace's configured path scope.resolve_for_writecanonicalizes an existing target or the nearest existing parent of a missing target. Missing reads fail, while missing writes have the explicitMissingWriteTargetstate.revalidatechecks that the canonical target, parent chain, scope, and missing/existing state did not change while authorization was pending.
Coding-tool adapters authorize the returned ResolvedWorkspacePath, revalidate that same object immediately before I/O, and execute against its canonical path. Edit operations additionally open file handles and detect content changes before writes. This reduces check/use disagreement and common symlink swaps, but portable path checks cannot remove every filesystem race. Hosts needing stronger guarantees should use descriptor-relative safe-open APIs or an operating-system sandbox.
Parent traversal is rejected even if lexical normalization would return inside the root unless the host enables unrestricted file access. Symlinks into an attached outside root still require both the root attachment and the policy's outside-workspace grant. Unrestricted file access and attached roots are explicit host choices; there is no implicit home-directory, sibling-directory, or absolute-path grant.
Explicit process context
CapabilityOperation::ExecuteProcess carries a ProcessExecution with:
- canonical working directory
ProcessInvocation, distinguishing direct execution from intentional shell execution- executable selection as an exact path or
PATHsearch - an argument vector separate from shell command text
- environment policy as empty, fully inherited, inherited-except-named, or an explicit inherited-name list
- output byte limit and optional wall-time limit
An approval UI can therefore identify the shell boundary, executable lookup, cwd, environment inheritance, timeout, and output budget without parsing shell text. Shell text remains available through a dedicated accessor for display or policy, but its Debug representation is redacted. Arguments are also omitted from Debug.
Rho's built-in shell and background-process adapters authorize these facts before spawning. They use a canonical workspace cwd, closed stdin, explicit shell arguments, bounded output, kill_on_drop, Unix process groups or Windows job objects, and descendant cleanup on timeout, stop, drop, or shutdown. Rho composes one explicit child-process environment at tool construction time: ProcessEnvironment::inherit_except(rho_providers::credential_env_vars()), so provider credential overrides are stripped from agent child processes. Generic SDK shell defaults remain ProcessEnvironment::InheritAll; security-sensitive hosts should require approval or inject a stricter process environment when constructing adapters.
Network and skill policy
ScopedWorkspacePolicy::allow_network_host accepts only parsed HTTP or HTTPS URLs without URL user information and compares a normalized exact host. It does not match suffixes. allow_network_tool is a separate grant for a built-in whose destination is internally managed, such as a configured search backend. Redirect, DNS, proxy, destination-IP, credential-forwarding, and response-size controls remain the network adapter's responsibility.
SDK-facing file skills are loaded only from .agents/skills/<validated-name>/SKILL.md beneath the canonical workspace, except for embedded built-ins. The path is canonicalized, authorized as Skill, revalidated, and then read. Skill authority does not imply ordinary read authority or instruction-discovery authority.
Instruction adapters use CapabilityRequest::instruction_discovery with a resolved path and scope. A custom SystemPrompt is already constructed host data, so the SDK does not retrospectively inspect or authorize files the host used to build it. Hosts implementing discovery must authorize before reading and list included PromptSource values in diagnostics.
Approvals, remembered rules, and audit diagnostics
Authorization follows this sequence:
- the tool submits a structured request
- policy allows, denies, or requires approval
- remembered approval is considered only after the current policy still requires approval
- the async host receives
ApprovalRequest, including the correlatedToolCallIdfor run-owned requests - cancellation drops the pending future and returns a typed cancelled authorization error
AllowOnce,AllowForSession, or denial completes exactly once
AllowForSession stores only an exact structured-request rule in that session. Changing a path, scope, command, executable, argument, cwd, environment mode, limit, URL, skill, source, or capability requires another approval. Rules are not persisted, copied to another session, or allowed to override a later policy denial.
approval_channel provides a bounded host queue. PendingApproval::respond accepts one response; repeating it returns the unused decision. The receiver skips requests whose authorization future was cancelled before host delivery. Dropping the receiver or responder produces a host denial instead of hanging. Cancelling a run drops the approval wait.
ToolContext::authorize returns AuthorizationOutcome or AuthorizationError, including typed policy, host, and cancellation denial sources. Built-ins convert denials to ToolErrorKind::PolicyDenied with a useful capability-specific message. The model receives that failed tool result and can continue, while the host receives typed ToolCompletion::Failure.
DiagnosticsSnapshot::approval_audit records bounded, ordered, secret-free decision facts: sequence, capability class, and sanitized result. It intentionally excludes reasons, paths, commands, arguments, environment values, URLs, skill names, and request bodies. Full approval requests remain available only to the approval handler and exact remembered rules remain in session memory.
See security and the threat model before enabling tools.