Repository navigation
Conversation
Merge main into dev after v0.30.0
A tool can now ask whether an action may go ahead and observe its call's cancellation without depending on an agent loop. `Approver` answers an `ApprovalRequest` with `Approval::Approved` or `Approval::Denied`, and `ToolContext::approve` routes the request to the approver its dispatcher attached with `with_approver`, attributing it to the call. A context with no approver fails with `ApprovalError::NoApprover` instead of allowing the action. `Canceller` and `CancelSignal` carry cancellation: the dispatcher attaches a signal with `with_cancellation`, a tool watches `cancellation()` for work that outlives its future, and a pending approval ends with `ApprovalError::Cancelled` when the call is cancelled.
`Approver::approve` now returns `Result<Approval, ApproverFailure>`. An approver that cannot decide, for example because the store it records requests in fails, returns `ApproverFailure` carrying its own error as the source, and `ToolContext::approve` surfaces it as `ApprovalError::Failed` instead of letting it pass for a denial.
feat(core): approval and cancellation on ToolContext
`Sandbox` starts a process on a backend (`Host`, `Container`, `Ssh`) under the sandbox's grants and network policy and hands back a `SandboxedChild`: byte-chunk streams, a `kill` that ends the whole tree and a repeatable `wait`. A host spawn runs under heel, a container spawn through the exec channel with a kill switch, and an SSH spawn records its process group on the server so `kill` reaches the remote tree and not only the local client. `Grants` is one live handle (`add_readable`, `add_writable`, `deny_read`, `revoke`, `derived`). A change applies to the next spawn; a derived view follows its parent's readable and denied paths but never widens them. The network lists give a verdict first and `DomainApprover::approve` decides what they leave open, with host, port and agent. The terminal builds its sandbox, grants, environment source, network lists, approver and audit log through these, and its container route is a `HostRoute`. BREAKING CHANGE: removed `TerminalGrants`, `GrantSet`, `SessionSandboxPolicy`, `ContainerIpcRoute` and `BridgeAddress`. `TerminalEnvSource` is now `EnvSource`. `PermissionHandler` loses `check_domain`, `domain_needs_approval`, `remember_domain`, `review` for domains and `authorize_domain`; `PermissionRequest::Domain` is gone. `TerminalTool::with_network_policy` and `with_domain_approver` take a `CompiledNetworkPolicy` and a `DomainApprover`.
External-hook reviewers move from `ReviewedPermission`'s domain methods to `ReviewedDomains`, a `DomainApprover` that asks the reviewers first and passes what they pass to an inner approver. The reviewer input keeps its `"kind":"domain"` shape. The CLI prompts for open domains through its own `DomainApprover`. BREAKING CHANGE: `ReviewedPermission` no longer answers domain checks; `ReviewRequest::request` is a `ReviewSubject`, and a reviewer's approval of a domain is no longer remembered by the inner route.
…rom aither-agent aither-agent is the agent loop and nothing else. The terminal builder, the `compact` and `subagent` tools with their definitions and router, the ask_user / workspace_request / tool_request protocols, the language, secret, runaway, host-load, disk, working-document, external-hook, command-catalog, output-url, browser-backend and judgment modules, the notices layer and the default system prompt are gone from aither entirely; the embedder supplies its own. The model tiers collapse to the one conversation model and the optional compaction model, `AgentKind` and the built-in instruction blocks leave the layout (instructions render in the order the embedder adds them), and the user-message time stamp, the language guard and the sample system prompt leave with them. A compaction finds the files tool results name through the embedder's `FileReferences` instead of scanning terminal output directories. BREAKING CHANGE: removes `TerminalAgentBuilder`, `Prepared`, `Unprepared`, `CompactTool`, `SubagentTool`, `SubagentArgs`, the subagent definition files, `AgentKind`, `BackgroundJobPolicy`, `CacheKeepAlive`, `ModelTier`, `TieredModels`, `AgentOptions`, `AgentSettings`, `Notices`, `TERMINAL_SYSTEM_PROMPT`, `wait_background` / `BackgroundWake` and the re-exported aither-sandbox crate. `AgentBuilder` is generic over `<M, H, C, Schema>`; `balanced_model`, `fast_model`, `tier`, `system_prompt`, `persona_prompt`, `agent_kind`, `terminal` and `dyn_terminal` are removed and `instruction`, `tagged_instruction`, `restore_dir` and `file_references` are added.
Execution of a readable file is not a property grants hold: what a process can read it can copy and run, and Linux runs it in place.
The loop reports through one event set (runs, turns, requests, blocks,
tool calls, pauses, compaction, context injection, checkpoints), every
event stamped with the path of the agent that emitted it. Embedder policy
moves behind a single `Hooks` trait (`before_request`, `before_tool`,
`after_tool`, `on_stop`, `on_turn_boundary`); a tool call's decision is
`Allow`, `Deny`, `Rewrite`, `Ask` or `Abort`, and `Ask` goes to the
agent's `Approver`, with `Paused`/`Resumed` emitted while it is pending.
A run is cancelled through `Run::cancel` or a `CancelHandle` and ends
with `RunEnded { Cancelled }`.
State, event and live-content sources are unified into one
`ContextSource` that returns stable or tail `ContextBlock`s at a
`Moment`, and learns which tail blocks went out through `committed`.
BREAKING CHANGE: `Hook`, `HCons`, `StopHook`, `AnyStopHook`,
`StateSource`, `EventSource`, `StateChange`, `ToolUseContext`,
`ToolResultContext`, `StopContext`, `CheckpointContext` and `StopReason`
are removed; `AgentBuilder::hook`, `stop_hook`, `state_source` and
`event_source` are replaced by `hooks` and `approver`; `AgentEvent` is
replaced by the new event set and `ContextSource` by the block-returning
trait.
A ScriptPolicy now owns the whole decision about a script. It sees where the script runs (sandbox, location) and the directory it starts in (ScriptDir: host, container or remote), answers Pass, Allow, Ask or Refuse, and is told how the script ended, including the directory the shell carries to the next one. The strictest decision wins, and every agent's terminal binds each policy to state of its own, fresh for a subagent. A script that crosses the sandbox boundary, unsandboxed or on a server, runs only after a policy allows it or an approver approves it. An ask goes to the approver of the tool call, or to the terminal's own, and a denial reaches the model with the reason the approver gave. The start directory is fixed once at admission and the end directory is settled under the carried-directory lock, so the end of one script is the start of the next under parallel calls. External hook reviewers become a ScriptPolicy (ReviewedScripts) that reads the calling agent's role from the call. BREAKING CHANGE: PermissionHandler, PermissionRequest, Review, CommandLocation, LocationApproval, PermissionError, NoopPermissionHandler and the permission event stream are removed; TerminalTool no longer takes a handler type, and approvals go through TerminalTool::with_approver or the call's ToolContext. BREAKING CHANGE: CommandPolicy, PolicyDecision and PolicyRefusal are replaced by ScriptPolicy, ScriptDecision and ScriptRefusal; ScriptCheck loses working_dir and role and gains sandbox, location, start and call; ScriptEnd carries the end directory and a Carry. The DirectoryReset job alert is removed; the reset is reported through ScriptEnd. BREAKING CHANGE: the SSH server first-use approval and its memory are removed; a script on a server is asked about like any script outside the sandbox. BREAKING CHANGE: aither-agent's ReviewedPermission is replaced by ReviewedScripts, and TerminalAgentBuilder and Agent::terminal lose the handler type parameter.
…t/708-script-policy
…ed events `SubagentRuns` records the children a tool starts, lists them and cancels one or all. `SubagentRun` hands the child its identity and cancel signal and brackets it on the parent's stream with `SubagentStarted` and `SubagentEnded`; the child's own events follow, tagged with its `AgentPath`. `ChildAgent` covers an in-process `Agent` and any external agent an embedder wraps. `AgentBuilder::subagents` joins the registry's events to the agent's stream while its tools run.
… assembly Tests for the terminal assembly, the subagent tool, the sandbox helpers and the old event set go with the code they exercised. New tests cover a run's events, denied and asked tool calls (Paused and Resumed while an approval is pending), cancellation, the stop hook, context sources, instruction order, and a child's events reaching its parent.
The CLI builds its agent with `Agent::builder`, asks for approval through a terminal `Approver` before every `command` call, and renders a run from its events. The ACP adapter and session map the new event set; ACP sessions read files and the web only, since their agent has no approval hooks. The browser tool keeps its host request channel in its own crate instead of borrowing it from aither-agent. BREAKING CHANGE: aither-browser no longer depends on aither-agent; `aither_agent::tool_request` is gone, and the channel types live in aither-browser. The CLI drops `--skills` and the `subagent` command.
…icy and ShellLimits The shell no longer ships a budget. `OutputPolicy` is a live handle holding the inline and preview bytes, the default and ceiling line counts, the read size, the media bounds and the job-list preview length; it has no `Default`, and the `OutputStore` the shell writes through carries it. `ShellLimits` holds the foreground wait, the timeout cap, the long-job threshold, the subagent import budget and the response window as live handles, also with no `Default`. `MonitorSettings` gives the monitor tool its name, description, cap and default interval. `max_lines` and the read tool's `max_bytes` are optional in their arguments and fall back to the policy. Every `DEFAULT_*` limit constant and `INLINE_OUTPUT_LIMIT` is removed. BREAKING CHANGE: `TerminalTool::new_in` and `new_exact`, `OutputStore::new`, `job_registry_channel`, `ReadTerminalDeltaTool::new`, `MonitorTool::new` and `output_saved_text` take the policy or limits they now read; the `DEFAULT_*` constants and `INLINE_OUTPUT_LIMIT` are gone; `TerminalArgs::max_lines` and `MonitorArgs::interval_secs` are `Option`; `MonitorRefusal::TimeoutOutOfRange` carries the cap.
AgentEvent, TimedEvent, AgentPath and every payload and id type derive Serialize and Deserialize under the new `serde` feature, so an embedder can send them across a process boundary without a mirror type. An approval error travels as its message. BREAKING CHANGE: AgentError::HookRejected::hook is a Cow<'static, str>.
preview_request_messages_with showed the previous request's tail. It now asks each context source through the new ContextSource::peek and the before_request hook for the blocks the next request carries, appends the stable ones and renders the tail fresh, without consuming a one-shot block. BREAKING CHANGE: ContextSource gains a required `peek` method.
The terminal arguments are renamed ShellArgs. `mode` becomes `sandbox` (`sandboxed` | `unsandboxed`, SandboxMode), `language` becomes `interpreter` (Interpreter), and `expect` is removed from the model-facing schema. TerminalMode, TerminalExecutionMode and ScriptLanguage leave the crate; the external reviewer's request carries `sandbox` and `interpreter`. BREAKING CHANGE: TerminalArgs, TerminalMode, TerminalExecutionMode and ScriptLanguage are replaced by ShellArgs, SandboxMode and Interpreter; the `mode`, `language` and `expect` argument keys are gone, and MonitorSource::Command takes `interpreter`.
…e loop Rebuilds the three suites against Agent, Hooks and ContextSource on a scripted model and an injected clock, without the terminal assembly: the idle refresh and its refusals, one idle compaction per gap and its events, the warmth handle, and the identity every call carries for the main agent and for a subagent started through SubagentRuns.
A job's ToolRegistry carries its JobOutput, and the registry lives as long as the IPC channels serving it: heel's router stays in a detached dispatch task until its accept thread winds down after the sandbox drops, and the container bridge's teardown is detached the same way. JobOutput held a strong lease, so a foreground command's task id could still be leased when its call returned, depending on how quickly that teardown was scheduled. JobOutput now refers to the lease weakly. The job's flow and its registry entry hold the word for as long as the job can be read; a report made after both let go is dropped, and can no longer reach a later job leasing the same word.
…t/708-script-policy
Tool::arguments_schema defaults to the schema derived from Arguments. ToolDefinition::new and the sandbox command wrappers read it through the tool, so the provider request, Tools::definitions and the command line see the override.
The container and SSH backends run what the embedder prepared as plain data: ContainerLaunch (tools directory and shell inside the container) and RemoteLaunch (tools directory, shell, exports and a writable directory on the server). Backend::Ssh carries the server's heel path and its launch instead of PreparedSsh. No backend type names PreparedTools or the tool distribution; PreparedTools converts to the launches from the outside. Refs #716 BREAKING CHANGE: Backend::Container { runtime, tools } is now { runtime, launch: ContainerLaunch }; Backend::Ssh { server, prepared } is now { server, heel, launch: RemoteLaunch }; remote_command takes a RemoteLaunch and returns askama::Error; PreparedSsh is no longer exported from spawn.
…t/716-backend-inputs
feat(agent)!: a general agent loop with events, hooks, approval and subagents
feat(core): a tool can supply its argument schema at runtime
…t/708-script-policy
…r connection The client owns a ControlMaster per server with its control socket in a private per-client directory, opened on first use under a lock so concurrent first commands start one master, and ended when the last clone of the client drops. An upload and its run no longer pay two handshakes. Closes #729
One lock across every server made all commands wait behind a slow or unreachable server's opening and spent an `ssh -O check` on each command. A server's master now opens once under its own once-only cell, a failed opening is retried by the next command, and an opened master goes straight to ControlMaster=auto.
…, and an SSH server is prepared on its first command `ShellBuilder::ipc(impl IpcProvider)` gives every script the shell runs on the host or in a container the router the provider returns for that launch's `LaunchInfo`; subagent shells share the provider. A server has no way back to this machine, so the shell asks for no router there and the SSH backend still refuses one with `SpawnError::Unsupported`. `add_ssh_server` takes an `impl PrepareSsh` instead of a `PreparedSsh`, so adding a server no longer waits on the remote. The preparation runs on the server's first command and is cached; commands that name the server together share it, and a failure fails the command with `TerminalError::SshPrepare` and is retried by the next one. `PreparedSsh` is itself a `PrepareSsh`. BREAKING CHANGE: `ShellParts` gains an input and `TerminalError` gains the `SshPrepare` variant. BREAKING CHANGE: `ShellSessionRegistry::add_ssh_server` takes an `impl PrepareSsh`; implement it where a server was prepared before adding.
perf(sandbox): commands to a server share one ssh connection
# Conflicts: # sandbox/src/ssh.rs
Jobs::output_appender hands out a JobOutputAppender that refers to the job weakly and appends into the same stream the process writes to, so a command served over IPC can show progress in the job that ran it. Appending to an ended job, or to a later job that reused the id, reports JobEnded.
A job registers under its pid, so it could not exist before its process did, and an IPC command called at the script's first instruction found no job to append to. The shell now records the launch before spawning: the job's lease is held while it starts, appends park until it registers and land ahead of the process's output, and a launch that never registers releases them as ended. Jobs::output_appender finds a launching job like a registered one.
The job registry keyed jobs by process id, so a new job whose pid the operating system (or a container backend) reused overwrote a finished job still held for its completion or a watcher, losing its output, completion and kill handle without an error. The registry now keys every job by the task id it leases, which is unique among live jobs, and keeps the pid as data on the job record. BREAKING CHANGE: the JobRegistry methods that took a pid to find a job (attach_terminal_input, attach_kill_switch, append_stdout, append_stderr, close_stdout, close_stderr, complete, fail, terminal_output, terminal_streams_closed, kill_timed_out, kill_runaway) take the task id instead; JobRegistry::get and JobRegistry::kill, which took a pid, are removed in favour of get_by_task_id and kill_by_task_id.
…bagent shell's jobs Every ShellEvent now carries the AgentPath of the shell that raised it (ShellEvent::agent and an accessor on each event struct): the main shell raises as AgentPath::main, a subagent shell as its parent's path plus the name given to Shell::subagent. AgentPath moves to aither-core beside AgentIdentity so aither-sandbox can name it; aither-agent re-exports it. A shell made by Shell::subagent kills its running jobs when its last handle is dropped, through a guard only the subagent's authority holds. A main shell's jobs keep running when it is dropped. BREAKING CHANGE: Shell::subagent takes the subagent's name as a third argument.
…est no longer needs
A run holds the agent, so an embedder could not export a checkpoint while it was driven, and a crash mid-run lost the turn's progress. The run now records a checkpoint at every AgentEvent::Checkpoint, before the event is queued, and Run::checkpoints returns a Checkpoints handle (like cancel_handle) whose latest() the event handler reads. BREAKING CHANGE: aither-agent depends on arc-swap, and Run gains a field.
…ell's sandbox A sandbox that only spawns programs needs no shell, so Backend::Host carries the heel binary alone. The shell a host Shell runs scripts under is an input of Shell::builder_on_host; Shell::builder stays for a container sandbox, and build refuses a constructor that does not fit the sandbox's backend. SpawnCommand::Script on a host sandbox stays SpawnError::Unsupported. Shell::sandbox returns the sandbox the shell runs in, so an embedder spawns its own processes in it without keeping a second copy. BREAKING CHANGE: Backend::Host loses its launch field, and a shell on a host sandbox is built with Shell::builder_on_host(sandbox, HostLaunch).
aither-sandbox gates what only the jobserver reads (container launch, the resolved re-export, heel grant imports); aither-agent and aither-acp declare the executor-core and tokio features their tests use; aither-webfetch imports its headless-only test policy under that feature.
…cache nothing reads, and install fish on Linux
The output pipeline collapsed every run of blank lines and turned JSON record arrays into TSV for every script, so a script that only reads a file handed the model text the file does not hold. Which scripts print content rather than command output is the embedder's judgement, so the shell now asks it. `ShellBuilder::treat_output` takes a `TreatOutput`, which sees each script before it runs through the same `ScriptCheck` the script policies get (the shell parsed once for all of them) and returns an `OutputTreatment`: `Command` runs today's pipeline, `Verbatim` keeps the terminal rendering alone (escape sequences and `\r` frames resolved, nothing else changed). Subagent shells share the embedder's choice. Without one every output is `Command`, as before. The treatment travels with the admitted script to `compress_text`; the internal `raw` flag becomes a `StdoutTreatment` that holds either the call's `raw` request or the chosen treatment, so the two cannot disagree. A verbatim output over the inline budget is still cut: the cut path writes the whole rendered text to disk, names the file, and shows the model a folded view or a preview. Nothing the model reads claims to be the file itself and the file on disk is exact, which is the same guarantee the command path gives for its lossy stages, so the cut stages stay for both treatments. `compress_text` takes the treatment as a third argument. Closes #735
The step's filter matched no test since the tool-distribution tests left, so it passed without checking anything. It now runs one test that downloads the pinned heel, checks its digest and verifies it against the linked library.
refactor(sandbox)!: the IPC bridge and the tool distribution leave aither
…sk-id # Conflicts: # sandbox/src/terminal.rs
fix(sandbox)!: the job registry keys a job by its task id, not its pid
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships the #593 redesign as one breaking release: aither carries the agent loop and the sandbox's general capabilities, and the embedder supplies its policy through typed extension points (shell assembly and policies, IPC router and environment, SSH preparation, output treatment, job events and output appenders, subagent shells, checkpoints). Terminal-first product pieces that lived here (the tool distribution, the IPC command bridge, wording) moved to the embedder. Also: the job registry is keyed by task id, container shims come from
heel shims(heel 0.9.5), and the CI drops jobs whose code left the repository.