rename elevate→bypass throughout

The sandbox escape mechanism is a bypass, not privilege elevation.
The old name caused the agent to confuse it with sudo.

- elevate/ → bypass/ (package, types, tests)
- elevate_notify.go → bypass_notify.go
- Namespace: /elevate → /bypass, session/*/elevate → session/*/bypass
- Tool arg: "elevated" → "bypass"
- Env: OLLIE_ELEVATE_SOCKET → OLLIE_BYPASS_SOCKET
- File: elevate-policy.yaml → bypass-policy.yaml
- All docs, prompts, and scripts updated
This commit is contained in:
Ollie Agent 2026-08-09 02:22:39 +02:00
parent d1907adfae
commit c7aea0db59
27 changed files with 179 additions and 179 deletions

View File

@ -67,7 +67,7 @@ Single Go module with one Git submodule (kde).
| `session/` | Go | Session lifecycle, config, persistence |
| `fs/` | Go | 9P filesystem tree for session namespace |
| `detach/` | Go | Background process management |
| `elevate/` | Go | Elevation broker (privilege escalation) |
| `bypass/` | Go | Bypass broker (sandbox escape approval) |
| `sandbox/` | Go | Landlock sandbox config |
| `cmd/olliesrv/` | Go | The main binary |
| `cmd/ollie-9p/` | Go | 9P client |

View File

@ -1,4 +1,4 @@
package elevate
package bypass
import (
"encoding/binary"

View File

@ -1,4 +1,4 @@
package elevate
package bypass
import (
"encoding/binary"

View File

@ -1,6 +1,6 @@
// Package elevate implements the elevation broker for running commands
// outside the Landlock sandbox with human-in-the-loop approval.
package elevate
package bypass
import (
"os"

View File

@ -1,4 +1,4 @@
package elevate
package bypass
import (
"os"

View File

@ -1,4 +1,4 @@
package elevate
package bypass
import (
"fmt"

View File

@ -5,27 +5,27 @@ import (
"sync"
"github.com/godbus/dbus/v5"
"ollie/elevate"
"ollie/bypass"
)
// elevateNotifier handles desktop notifications for elevation requests
// bypassNotifier handles desktop notifications for bypass requests
// and maps notification action callbacks back to the broker.
type elevateNotifier struct {
type bypassNotifier struct {
conn *dbus.Conn
broker *elevate.Broker
broker *bypass.Broker
mu sync.Mutex
notifID map[uint32]string // notification ID -> request ID
}
var notifier *elevateNotifier
var notifier *bypassNotifier
// initElevateNotifier sets up the notification action listener.
func initElevateNotifier(conn *dbus.Conn, broker *elevate.Broker) {
// initBypassNotifier sets up the notification action listener.
func initBypassNotifier(conn *dbus.Conn, broker *bypass.Broker) {
if conn == nil || broker == nil {
return
}
notifier = &elevateNotifier{
notifier = &bypassNotifier{
conn: conn,
broker: broker,
notifID: make(map[uint32]string),
@ -42,15 +42,15 @@ func initElevateNotifier(conn *dbus.Conn, broker *elevate.Broker) {
go notifier.listenSignals(ch)
}
// notifyElevation sends a desktop notification for an elevation request.
func notifyElevation(req *elevate.Request) {
// notifyBypass sends a desktop notification for a bypass request.
func notifyBypass(req *bypass.Request) {
if notifier == nil {
return
}
notifier.sendNotification(req)
}
func (n *elevateNotifier) sendNotification(req *elevate.Request) {
func (n *bypassNotifier) sendNotification(req *bypass.Request) {
obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
summary := "Elevation Request"
@ -91,7 +91,7 @@ func (n *elevateNotifier) sendNotification(req *elevate.Request) {
n.mu.Unlock()
}
func (n *elevateNotifier) listenSignals(ch chan *dbus.Signal) {
func (n *bypassNotifier) listenSignals(ch chan *dbus.Signal) {
for sig := range ch {
switch sig.Name {
case "org.freedesktop.Notifications.ActionInvoked":
@ -110,16 +110,16 @@ func (n *elevateNotifier) listenSignals(ch chan *dbus.Signal) {
continue
}
var res elevate.Resolution
var res bypass.Resolution
switch action {
case "approve":
res = elevate.ResolveApprove
res = bypass.ResolveApprove
case "deny":
res = elevate.ResolveDeny
res = bypass.ResolveDeny
case "persist":
res = elevate.ResolvePersist
res = bypass.ResolvePersist
default:
res = elevate.ResolveDeny
res = bypass.ResolveDeny
}
n.broker.Resolve(reqID, res)
@ -142,7 +142,7 @@ func (n *elevateNotifier) listenSignals(ch chan *dbus.Signal) {
// reason 2 = dismissed by user, 1 = expired, 3 = closed by call, 4 = undefined
// Treat dismiss as deny
if reason == 2 {
n.broker.Resolve(reqID, elevate.ResolveDeny)
n.broker.Resolve(reqID, bypass.ResolveDeny)
}
}
}

View File

@ -15,7 +15,7 @@ import (
"9fans.net/go/plan9/client"
"ollie/agent"
"ollie/elevate"
"ollie/bypass"
"ollie/env"
"ollie/fs"
olog "ollie/log"
@ -63,7 +63,7 @@ func runServer(sockPath string) {
sessionsDir := paths.DataDir() + "/sessions"
// Elevate broker (initialized after manager; closures capture the pointer).
var elevateBroker *elevate.Broker
var bypassBroker *bypass.Broker
daemonCtx, daemonCancel := context.WithCancel(context.Background())
defer daemonCancel()
@ -81,22 +81,22 @@ func runServer(sockPath string) {
})
var srv *Server
// Server creation deferred until after elevate broker is ready (see below)
// Server creation deferred until after bypass broker is ready (see below)
// Start elevation broker
// Start bypass broker
xdg := os.Getenv("XDG_RUNTIME_DIR")
if xdg == "" {
xdg = fmt.Sprintf("/run/user/%d", os.Getuid())
}
elevateSocket := filepath.Join(xdg, "ollie", "elevate.sock")
os.Setenv("OLLIE_ELEVATE_SOCKET", elevateSocket) //nolint:errcheck
policyPath := filepath.Join(paths.DataDir(), "elevate-policy.yaml")
bypassSocket := filepath.Join(xdg, "ollie", "bypass.sock")
os.Setenv("OLLIE_BYPASS_SOCKET", bypassSocket) //nolint:errcheck
policyPath := filepath.Join(paths.DataDir(), "bypass-policy.yaml")
{
notifyFn := func(req *elevate.Request) {
notifyElevation(req)
notifyFn := func(req *bypass.Request) {
notifyBypass(req)
}
credentialPrompt := func(req *elevate.Request) (string, error) {
credentialPrompt := func(req *bypass.Request) (string, error) {
// Try kdialog (KDE), then zenity (GTK), then terminal fallback
title := fmt.Sprintf("sudo password for: %s", req.Cmd)
if path, err := exec.LookPath("kdialog"); err == nil {
@ -116,8 +116,8 @@ func runServer(sockPath string) {
return "", fmt.Errorf("no credential provider available (install kdialog or zenity)")
}
var err error
elevateBroker, err = elevate.NewBroker(elevate.BrokerConfig{
SocketPath: elevateSocket,
bypassBroker, err = bypass.NewBroker(bypass.BrokerConfig{
SocketPath: bypassSocket,
PolicyPath: policyPath,
Notify: notifyFn,
Credential: credentialPrompt,
@ -127,10 +127,10 @@ func runServer(sockPath string) {
if err != nil {
fmt.Fprintf(os.Stderr, "warning: %v\n", err)
} else {
defer elevateBroker.Close()
// Desktop notifications for elevation prompts (uses D-Bus notifications API directly)
defer bypassBroker.Close()
// Desktop notifications for bypass prompts (uses D-Bus notifications API directly)
if conn, err := dbus.SessionBus(); err == nil {
initElevateNotifier(conn, elevateBroker)
initBypassNotifier(conn, bypassBroker)
}
}
}

View File

@ -8,7 +8,7 @@ Base agent with no specializations. Provides the standard ollie toolset and prom
## coding
Coding-focused agent. Extends default with agent instructions covering code modification discipline, security practices, and exploratory reading habits. Also enables the `execute_elevated_bash` trusted tool for operations requiring elevated privileges, and configures an MCP server for elevated operations.
Coding-focused agent. Extends default with agent instructions covering code modification discipline, security practices, and exploratory reading habits. Also enables the `execute_bypass_bash` trusted tool for operations requiring bypass privileges, and configures an MCP server for bypass operations.
## System prompt
@ -24,7 +24,7 @@ Hooks are lifecycle callbacks executed at specific points:
| Hook | When | Use case |
|---|---|---|
| `agentSpawn` | Session creation | Start elevation adapter, initialize state |
| `agentSpawn` | Session creation | Start bypass adapter, initialize state |
| `postTurn` | After each turn | Task list display, logging |
| `preCompact` | Before context compaction | Save state |
| `postCompact` | After context compaction | Notify user |

View File

@ -27,10 +27,10 @@ set -euo pipefail
# ── Path classification ─────────────────────────────────────────
# Root-level entries (no context needed)
ROOT_ENTRIES="session elevate agents backends models help ctl eventwait complete generate route aliases tools"
ROOT_ENTRIES="session bypass agents backends models help ctl eventwait complete generate route aliases tools"
# Session-level files (exist directly under session/{name}/)
SESSION_FILES="env paused connected ctl name id elevate"
SESSION_FILES="env paused connected ctl name id bypass"
# Agent-level files (exist under session/{name}/agent/{name}/)
AGENT_FILES="prompt fifo chat chat.raw statewait log plan cfg ctl cwd stats models systemprompt tools name id proc"

View File

@ -1,7 +1,7 @@
#!/bin/bash
# Shell — execute a single bash command in a sandboxed environment.
# Already running inside the sandbox applied by toolsrv.
# Reads JSON args from stdin: {"cmd": "...", "timeout": 30, "elevated": false}
# Reads JSON args from stdin: {"cmd": "...", "timeout": 30, "bypass": false}
#
# This is the bootstrap primitive. All other tool scripts use this
# to execute commands on the system.

View File

@ -18,9 +18,9 @@
"type": "string",
"description": "Sandbox profile name (default: default)."
},
"elevated": {
"bypass": {
"type": "boolean",
"description": "Run outside the sandbox via elevation broker."
"description": "Run outside the sandbox via bypass broker."
}
}
},

View File

@ -4,7 +4,7 @@ ollie's design philosophy is Plan 9 / Acme / Unix: a small substrate that integr
The key difference from the Emacs model: Emacs extensibility means building capabilities *on top of* its Lisp interpreter and text primitives. Ollie's extensibility means **not** building capabilities into the core at all — instead, the core provides integration surfaces (9P filesystem, agent loop) and lets the surrounding system handle everything else. This is the Acme approach: a lightweight framework that delegates to external programs via a shared filesystem namespace.
The primary integration philosophy is Plan 9's "everything is a file." `olliesrv` exposes agent state and behaviors as files in a 9P namespace. Any program that can read and write files can drive an agent: shell scripts, editors, web apps, cron, containers.
All clients communicate via 9P exclusively. Desktop notifications use D-Bus directly (org.freedesktop.Notifications) for elevation prompts only — this is the last remaining D-Bus dependency.
All clients communicate via 9P exclusively. Desktop notifications use D-Bus directly (org.freedesktop.Notifications) for bypass prompts only — this is the last remaining D-Bus dependency.
Design principles:
- **Small extensible core.** The agent runtime is minimal; capabilities come from composing external scripts.
- **Zero built-in tools.** No tools are compiled into the Go binary. Every tool — including `shell` and `reasoning_think` — is an external script loaded dynamically via the 9P filesystem. The core is pure coordination: stream LLM → dispatch tool calls → loop.
@ -26,7 +26,7 @@ ollie/
│ ├── rootfiles.go Root-level handlers (backends, models, eventwait, complete, generate, route)
│ ├── sessionfiles.go Session-level handlers (env, ctl, plan, agent/, ...)
│ ├── agentfiles.go Agent-level handlers (prompt, chat, state, cfg, ...)
│ ├── elevatefiles.go Elevation handlers (policy, pending)
│ ├── bypassfiles.go Bypass handlers (policy, pending)
│ ├── procfiles.go Process handlers
│ ├── lifecycle.go Session create/kill/rename/shutdown + event ring
│ ├── newroot.go NewRoot — tree construction + persistence restore
@ -36,7 +36,7 @@ ollie/
│ ├── fs.go 9P File/FileConfig implementations
│ └── format.go Event formatting helpers
├── detach/ Background process management (ring buffer, signal)
├── elevate/ Elevation broker (privilege escalation daemon)
├── bypass/ Bypass broker (privilege escalation daemon)
├── sandbox/ Landlock sandbox config YAML
├── env/ Environment variable loading
├── log/ Structured logging
@ -108,7 +108,7 @@ The core is a single Go module (`ollie`) with no binary. Binaries live in `cmd/`
| `toolsrv/` | Tool server: dynamic tool dispatch, sandboxed execution, result tiering, remote execution (ollie-remote) |
| `session/` | Session lifecycle, config, persistence |
| `detach/` | Background process management (ring buffer, signal) |
| `elevate/` | Elevation broker (privilege escalation daemon, policy) |
| `bypass/` | Bypass broker (privilege escalation daemon, policy) |
| `sandbox/` | Landrun sandbox configuration and command wrapping |
| `env/` | Session environment helpers |
| `log/` | Structured logging |
@ -201,9 +201,9 @@ Agent configs declare prompts as a JSON array of shell commands. Each command is
├── complete request-response: code completion
├── generate request-response: one-shot LLM generation
├── route request-response: model routing
├── elevate/
│ ├── policy global elevation policy
│ └── pending/{id} pending elevation requests (approve/deny/persist)
├── bypass/
│ ├── policy global bypass policy
│ └── pending/{id} pending bypass requests (approve/deny/persist)
└── session/
├── new write key=value to create session
├── idx session index (one line per agent)
@ -213,7 +213,7 @@ Agent configs declare prompts as a JSON array of shell commands. Each command is
│ ├── plan session-scoped markdown checklist
│ ├── id immutable session UUID
│ ├── name mutable session name (write to rename)
│ ├── elevate per-session elevation policy
│ ├── bypass per-session bypass policy
│ └── agent/
│ ├── new write config to create agent (rdwr)
│ └── {aname}/

View File

@ -64,7 +64,7 @@ flowchart TB
SESSFILES["sessionfiles.go\nsession file handlers"]
AGENTFILES["agentfiles.go\nagent file handlers"]
ROOTFILES["rootfiles.go\nroot file handlers"]
ELEVATEFILES["elevatefiles.go\nelevation handlers"]
BYPASSFILES["bypassfiles.go\nbypass handlers"]
PROCFILES["procfiles.go\nprocess handlers"]
PERSIST["persist.go\nsession persistence"]
FS_GO["fs.go\n9P File/FileConfig"]
@ -77,7 +77,7 @@ flowchart TB
subgraph Support["Supporting Packages"]
DETACH["detach/\nprocess management"]
ELEVATE["elevate/\nelevation broker"]
BYPASS["bypass/\nbypass broker"]
SANDBOX["sandbox/\nLandlock config"]
ENV["env/\nenvironment"]
LOG["log/\nstructured logging"]
@ -90,7 +90,7 @@ flowchart TB
Fs --> Session
Toolsrv --> SANDBOX
Toolsrv --> DETACH
Toolsrv --> ELEVATE
Toolsrv --> BYPASS
```
## The Agent Struct (`agent/agent.go`)
@ -418,7 +418,7 @@ Runs a single bash command in a sandbox.
"cmd": "...",
"timeout": 30,
"sandbox": "default",
"elevated": false
"bypass": false
}
```
@ -453,9 +453,9 @@ Built-in cold tools (results consumed immediately, don't need verbatim retention
Custom tools declare their tier via `"tier": "cold|warm|hot"` in their `.meta` file.
### Elevation
### Bypass
Steps marked `elevated: true` bypass the sandbox entirely. They are dispatched to the elevation backend (`x/elevate`) which runs outside landrun. Only bash is supported for elevated steps.
Steps marked `bypass: true` bypass the sandbox entirely. They are dispatched to the bypass backend (`x/bypass`) which runs outside landrun. Only bash is supported for bypass steps.
### Remote Execution

View File

@ -97,7 +97,7 @@ type HandlerCtx struct {
ToolReg *toolsrv.Registry
Skills *skills.Registry
Models *ModelCache
Elevate *elevate.Broker
Elevate *bypass.Broker
Invalidate func()
Shutdown func()

View File

@ -32,7 +32,7 @@ gantt
D-Bus removed :done, 2026-07-31, 1d
section Late Evolution
Remote execution :done, 2026-07-05, 15d
Elevation broker :done, 2026-07-10, 15d
Bypass broker :done, 2026-07-10, 15d
Tool registry (lazy) :done, 2026-07-20, 10d
FUSE → 9P client :done, 2026-07-25, 5d
The Great Flattening :done, 2026-07-29, 2d
@ -155,7 +155,7 @@ flowchart LR
AGT["Agent"]
SANDBOX["landrun sandbox\n(restricted fs)"]
TOOLS["Tool scripts"]
ELEV["x/elevate\n(socket broker)"]
BYPASS["x/bypass\n(socket broker)"]
PRIV["Privileged Action"]
end
AGT --> SANDBOX --> TOOLS
@ -165,9 +165,9 @@ Evolution:
1. No sandboxing initially
2. YAML-based sandbox configs (landrun) for execute_code
3. Per-session 9P identity — agents can't read other sessions' tools
4. Elevation broker: socket-based, user-confirmed privilege escalation
4. Bypass broker: socket-based, user-confirmed privilege escalation
5. SSH agent proxy added then removed (too much attack surface)
6. Final: integrated elevation broker replaces separate superpowerd adapter
6. Final: integrated bypass broker replaces separate superpowerd adapter
## Phase 8: D-Bus as Second Control Plane (Jun 25 – Jul)
Originally everything was 9P-only. D-Bus was added for desktop integration:
1. Separate `ollie-dbus` daemon (ollied)
@ -348,7 +348,7 @@ the remote host may have different tools, package managers, or init systems.
### Sudo credential broker
Tools declare `"sudo": true` in their `.meta` to require root privileges.
Dispatch becomes a two-gate chain: elevation approval → credential prompt → `sudo -S`.
Dispatch becomes a two-gate chain: bypass approval → credential prompt → `sudo -S`.
Credentials are requested via `kdialog`/`zenity` on the local desktop, or forwarded
over SSH for remote execution. The tool itself has no knowledge of sudo — the
privilege wrapping is entirely in the dispatch layer.
@ -364,7 +364,7 @@ privilege wrapping is entirely in the dispatch layer.
checklist file per session.
6. **Single control plane — 9P for everything.
Streaming via blocking reads. No polling.
7. **Security by default** — sandboxed execution with explicit elevation,
7. **Security by default** — sandboxed execution with explicit bypass,
per-session identity.
## Dead Ends and Reversals
| What | Why it was removed |
@ -408,7 +408,7 @@ Key changes:
- **dbus/ package**: deleted (-771 lines from server).
The only remaining godbus usage: `org.freedesktop.Notifications` for
elevation prompts (desktop integration, not ollie protocol).
bypass prompts (desktop integration, not ollie protocol).
Lessons:
- 9P's request-response model gives natural streaming for free.
@ -488,11 +488,11 @@ a fully-wired `*fs.Tree`.
### Package consolidation
- Entire `fs/session/` sub-package flattened into `fs/` — 6 files deleted
- `cmd/olliesrv/elevate_tree.go` — elevation tree moved to `fs/elevatefiles.go`
- `cmd/olliesrv/bypass_tree.go` — bypass tree moved to `fs/bypassfiles.go`
- `cmd/olliesrv/server.go` simplified from ~820+ lines to ~200 lines of thin
9P protocol handling; all filesystem logic is now in `fs/`
- Handler files organized by scope: `rootfiles.go`, `sessionfiles.go`,
`agentfiles.go`, `elevatefiles.go`, `procfiles.go`
`agentfiles.go`, `bypassfiles.go`, `procfiles.go`
### Net result
- **−3,152 lines** deleted, **+2,086 lines** added across 24 files
@ -523,7 +523,7 @@ Following the EDSL conversion, the 9P protocol server itself
### Result
- `server.go` reduced from ~820 to ~200 lines
- `main.go` simplified as elevation tree wiring moved to `fs/`
- `main.go` simplified as bypass tree wiring moved to `fs/`
- Server is now a thin 9P2000 protocol translator, not a filesystem
## Phase 17: Eventwait Redesign & Chat Log Format (Aug 2)
@ -591,9 +591,9 @@ The KDE frontend received extensive updates across both days:
├── complete request-response: code completion
├── generate request-response: one-shot LLM generation
├── route request-response: model routing
├── elevate/
│ ├── policy global elevation policy
│ └── pending/{sname} pending elevation requests
├── bypass/
│ ├── policy global bypass policy
│ └── pending/{sname} pending bypass requests
└── session/
├── new write key=value to create session
├── idx session index (one line per agent)
@ -603,7 +603,7 @@ The KDE frontend received extensive updates across both days:
│ ├── plan session-scoped markdown checklist
│ ├── id immutable session UUID
│ ├── name mutable session name (write to rename)
│ ├── elevate per-session elevation policy
│ ├── bypass per-session bypass policy
│ └── agent/
│ ├── new write config to create agent (rdwr)
│ └── {aname}/
@ -632,7 +632,7 @@ ollie/ ← single Go module
│ ├── rootfiles.go Root-level handlers
│ ├── sessionfiles.go Session-level handlers
│ ├── agentfiles.go Agent-level handlers
│ ├── elevatefiles.go Elevation handlers
│ ├── bypassfiles.go Bypass handlers
│ ├── procfiles.go Process handlers
│ ├── lifecycle.go Create/kill/rename + event ring
│ ├── newroot.go NewRoot constructor
@ -642,7 +642,7 @@ ollie/ ← single Go module
│ ├── fs.go 9P File/FileConfig
│ └── format.go Event formatting
├── detach/ ← background process management
├── elevate/ ← elevation broker
├── bypass/ ← bypass broker
├── sandbox/ ← landrun sandbox config
├── env/ ← environment helpers
├── log/ ← structured logging
@ -681,7 +681,7 @@ ollie/ ← single Go module
| Script namespaces (s/, u/, x/) | Replaced by 9P request-response files |
| D-Bus adapter | 9P streaming is superior; no polling, no offset tracking, no frozen GUIs |
| `fs/session/` sub-package | Flattened into `fs/` — no more sub-package indirection |
| `cmd/olliesrv/elevate_tree.go` | Elevation tree handlers moved to `fs/elevatefiles.go` |
| `cmd/olliesrv/bypass_tree.go` | Bypass tree handlers moved to `fs/bypassfiles.go` |
| Per-session/agent timestamp IDs | Replaced by UUIDv4 — immutable `id` + mutable `name` |
| Single-step session creation | Split into `session/new` + `session/{name}/agent/new` |
| D-Bus-driven KDE frontends | Rewritten on pure 9P via plan9port `9p` binary |
@ -884,7 +884,7 @@ fs/
│ ├── root.go
│ ├── perm.go
│ └── ...
└── elevatefiles.go
└── bypassfiles.go
```
**After** (flat, by scope):
@ -896,7 +896,7 @@ fs/
├── rootfiles.go root-level handlers (backends, models, help, ctl)
├── sessionfiles.go session-level handlers (env, ctl, name)
├── agentfiles.go agent-level handlers (prompt, chat, state, tools)
├── elevatefiles.go elevation broker handlers
├── bypassfiles.go bypass broker handlers
└── procfiles.go detached process handlers
```

View File

@ -55,7 +55,7 @@ Each tool has a `.meta` JSON sidecar file alongside the executable:
| `tier` | Result cacheability: `cold`, `warm`, or `hot` (default: hot) |
| `readOnly` | Safe for parallel execution with other read tools |
| `cmd` | Executable path or name override |
| `sudo` | Requires root privileges; implies elevation |
| `sudo` | Requires root privileges; implies bypass |
| `variants` | Conditional definitions for heterogeneous hosts |
### Runtime access

View File

@ -14,7 +14,7 @@ This means any operation an MCP server can perform, a tool script can also perfo
- An MCP server that searches the web becomes a `s/{id}/t/web_search` script that calls `curl` or a search CLI.
- An MCP server that manages files is redundant — `s/{id}/t/file_read`, `s/{id}/t/file_write`, `s/{id}/t/file_edit` already exist.
The `x/elevate` plugin is a concrete example: it replaced an MCP server that ran commands outside the sandbox with a 50-line shell script. Same capability, no protocol overhead, composable with pipes, debuggable with `cat`.
The `x/bypass` plugin is a concrete example: it replaced an MCP server that ran commands outside the sandbox with a 50-line shell script. Same capability, no protocol overhead, composable with pipes, debuggable with `cat`.
## What MCP adds vs. what ollie already has

View File

@ -316,7 +316,7 @@ env:
|---|---|
| OLLIE_* paths (10+) | Just OLLIE_TOOLS_PATH |
| D-Bus socket | No |
| Elevation socket | No |
| Bypass socket | No |
| 9P mount paths | No |
| Session/transcript dirs | No |
| Editor integration | No |
@ -333,7 +333,7 @@ The remote config is ~20 lines vs ~170 locally.
## Open Questions
- **Elevation on remote**: `elevated: true` escapes the sandbox. On remote, this means escaping the remote sandbox. The elevation socket doesn't exist remotely. Options: skip elevation, or run a remote elevation adapter.
- **Bypass on remote**: `bypass: true` escapes the sandbox. On remote, this means escaping the remote sandbox. The bypass socket doesn't exist remotely. Options: skip bypass, or run a remote bypass adapter.
- **Detached processes**: Currently tracked locally. Remote detached processes need their own lifecycle.
- **Multiple remotes per session**: Useful? Or one remote per session is sufficient?
- **Fallback**: If SSH drops mid-execution, retry? Fail the tool call? The agent can handle tool errors gracefully already.

View File

@ -54,12 +54,12 @@ The `.meta` file (`file_glob.meta`):
```
Everything is sandboxed by default via Landlock. Tools that need to escape
request elevation explicitly (`"elevated": true`).
request bypass explicitly (`"bypass": true`).
### Privileged tools: `sudo`
Tools that need root privileges declare `"sudo": true` in their `.meta`. The
dispatch chain becomes a two-gate sequence: elevation approval (escape sandbox)
dispatch chain becomes a two-gate sequence: bypass approval (escape sandbox)
→ credential prompt → `sudo -S` execution. The tool itself has no knowledge of
sudo — the privilege wrapping is entirely in the dispatch layer.

View File

@ -60,7 +60,7 @@ reads only `.meta` files — it never inspects the executable itself.
| `tier` | `"hot"\|"warm"\|"cold"` | Context retention tier (default: hot) |
| `readOnly` | bool | Safe for parallel execution with other read tools |
| `cmd` | string | Executable path or name (see Resolution below) |
| `sudo` | bool | Requires root privileges; implies elevation |
| `sudo` | bool | Requires root privileges; implies bypass |
| `variants` | array | Conditional definitions for heterogeneous hosts (see Variants below) |
## Executable resolution
@ -234,16 +234,16 @@ stdin, it's a valid tool.
### Elevation: escape the sandbox
Tools that need to escape the sandbox declare `"elevated": true` in their
`.meta`, or the agent passes `"elevated": true` in the tool call. The
dispatcher requests approval from the **elevation broker**, which notifies the
Tools that need to escape the sandbox declare `"bypass": true` in their
`.meta`, or the agent passes `"bypass": true` in the tool call. The
dispatcher requests approval from the **bypass broker**, which notifies the
user (desktop notification, D-Bus, etc.). If approved, the tool runs outside
Landlock but still as the current user.
```json
{
"description": "Write to a protected path.",
"elevated": true,
"bypass": true,
"cmd": "/usr/local/bin/my-tool"
}
```
@ -251,11 +251,11 @@ Landlock but still as the current user.
### Sudo: run as root
Tools that need root privileges declare `"sudo": true` in their `.meta`.
This **implies elevation** — can't sudo inside a sandbox. The dispatch chain
This **implies bypass** — can't sudo inside a sandbox. The dispatch chain
becomes two sequential gates:
```
tool call → elevation gate → credential gate → sudo -S tool
tool call → bypass gate → credential gate → sudo -S tool
```
1. **Elevation gate** — broker asks: "approve escaping the sandbox?" User
@ -317,7 +317,7 @@ Tools receive:
| Variable | Purpose |
|---|---|
| `OLLIE_TOOLS_PATH` | Path to the tools directory |
| `OLLIE_ELEVATE_SOCKET` | Unix socket for elevation broker |
| `OLLIE_BYPASS_SOCKET` | Unix socket for bypass broker |
| `PWD` | Session's current working directory |
## Bundled examples

View File

@ -12,7 +12,7 @@ import (
"ollie/agent"
"ollie/backend"
"ollie/elevate"
"ollie/bypass"
"ollie/fsedsl"
"ollie/paths"
"ollie/session"
@ -150,33 +150,33 @@ func readAliases(_ HandlerCtx) ([]byte, error) {
return []byte(sb.String()), nil
}
var errNoElevate = fmt.Errorf("elevate broker not available")
var errNoBypass = fmt.Errorf("bypass broker not available")
func readElevatePolicy(ctx HandlerCtx) ([]byte, error) {
if ctx.Elevate == nil {
return nil, errNoElevate
func readBypassPolicy(ctx HandlerCtx) ([]byte, error) {
if ctx.Bypass == nil {
return nil, errNoBypass
}
p := ctx.Elevate.GlobalPolicy().Global()
p := ctx.Bypass.GlobalPolicy().Global()
return p.Marshal()
}
func writeElevatePolicy(ctx HandlerCtx, data []byte) error {
if ctx.Elevate == nil {
return errNoElevate
func writeBypassPolicy(ctx HandlerCtx, data []byte) error {
if ctx.Bypass == nil {
return errNoBypass
}
var p elevate.Policy
if err := elevate.ParsePolicy(data, &p); err != nil {
var p bypass.Policy
if err := bypass.ParsePolicy(data, &p); err != nil {
return err
}
return ctx.Elevate.GlobalPolicy().SetGlobal(p)
return ctx.Bypass.GlobalPolicy().SetGlobal(p)
}
// elevateBindings returns bindings for pending elevation requests.
func elevateBindings(ctx HandlerCtx) ([]Binding, error) {
if ctx.Elevate == nil {
return nil, errNoElevate
// bypassBindings returns bindings for pending bypass requests.
func bypassBindings(ctx HandlerCtx) ([]Binding, error) {
if ctx.Bypass == nil {
return nil, errNoBypass
}
pending := ctx.Elevate.Pending()
pending := ctx.Bypass.Pending()
out := make([]Binding, len(pending))
for i, r := range pending {
req := r // capture
@ -191,26 +191,26 @@ func elevateBindings(ctx HandlerCtx) ([]Binding, error) {
return out, nil
}
func readElevateRequest(ctx HandlerCtx) ([]byte, error) {
req := ctx.Data.(*elevate.Request)
func readBypassRequest(ctx HandlerCtx) ([]byte, error) {
req := ctx.Data.(*bypass.Request)
return []byte(req.Summary() + "\n"), nil
}
func writeElevateRequest(ctx HandlerCtx, data []byte) error {
req := ctx.Data.(*elevate.Request)
func writeBypassRequest(ctx HandlerCtx, data []byte) error {
req := ctx.Data.(*bypass.Request)
cmd := strings.TrimSpace(string(data))
var res elevate.Resolution
var res bypass.Resolution
switch cmd {
case "approve":
res = elevate.ResolveApprove
res = bypass.ResolveApprove
case "deny":
res = elevate.ResolveDeny
res = bypass.ResolveDeny
case "persist":
res = elevate.ResolvePersist
res = bypass.ResolvePersist
default:
return fmt.Errorf("unknown resolution: %s (use approve/deny/persist)", cmd)
}
if !ctx.Elevate.Resolve(req.ID, res) {
if !ctx.Bypass.Resolve(req.ID, res) {
return fmt.Errorf("request already resolved or timed out")
}
return nil
@ -374,23 +374,23 @@ func readSessionID(ctx HandlerCtx) ([]byte, error) {
return []byte(ctx.Session.ID() + "\n"), nil
}
func readSessionElevate(ctx HandlerCtx) ([]byte, error) {
if ctx.Elevate == nil {
return nil, fmt.Errorf("elevate broker not available")
func readSessionBypass(ctx HandlerCtx) ([]byte, error) {
if ctx.Bypass == nil {
return nil, fmt.Errorf("bypass broker not available")
}
p := ctx.Elevate.SessionPolicy(ctx.Session.ID())
p := ctx.Bypass.SessionPolicy(ctx.Session.ID())
return p.Marshal()
}
func writeSessionElevate(ctx HandlerCtx, data []byte) error {
if ctx.Elevate == nil {
return fmt.Errorf("elevate broker not available")
func writeSessionBypass(ctx HandlerCtx, data []byte) error {
if ctx.Bypass == nil {
return fmt.Errorf("bypass broker not available")
}
var p elevate.Policy
if err := elevate.ParsePolicy(data, &p); err != nil {
var p bypass.Policy
if err := bypass.ParsePolicy(data, &p); err != nil {
return err
}
ctx.Elevate.SetSessionPolicy(ctx.Session.ID(), p)
ctx.Bypass.SetSessionPolicy(ctx.Session.ID(), p)
return nil
}

View File

@ -4,7 +4,7 @@ import (
"context"
"os/user"
"ollie/elevate"
"ollie/bypass"
"ollie/fsedsl"
olog "ollie/log"
"ollie/session"
@ -20,7 +20,7 @@ type Config struct {
SessionsDir string
Yolo bool
ModelCache *ModelCache
ElevateBroker *elevate.Broker
BypassBroker *bypass.Broker
Shutdown func()
Invalidate func()
}
@ -62,7 +62,7 @@ func NewRoot(cfg Config) *Tree {
RootState: rs,
Log: cfg.Log,
Models: cfg.ModelCache,
Elevate: cfg.ElevateBroker,
Bypass: cfg.BypassBroker,
Shutdown: cfg.Shutdown,
Invalidate: cfg.Invalidate,
}

View File

@ -8,7 +8,7 @@ import (
"context"
"ollie/agent"
"ollie/elevate"
"ollie/bypass"
"ollie/fsedsl"
olog "ollie/log"
)
@ -24,7 +24,7 @@ type HandlerCtx struct {
RootState *RootState
Log *olog.Logger
Models *ModelCache
Elevate *elevate.Broker
Bypass *bypass.Broker
Invalidate func() // called by root ctl "invalidate"
Shutdown func() // called by root ctl "kill"
@ -127,20 +127,20 @@ FileNode("generate", 0666,
Read(readAliases),
),
DirNode("elevate",
Doc("Elevation broker for sandboxed command approval"),
DirNode("bypass",
Doc("Bypass broker for sandboxed command approval"),
FileNode("policy", 0666,
Doc("Global elevation policy. Read: current policy. Write: key=value to update."),
Read(readElevatePolicy),
Write(writeElevatePolicy),
Doc("Global bypass policy. Read: current policy. Write: key=value to update."),
Read(readBypassPolicy),
Write(writeBypassPolicy),
),
Each("{reqid}", elevateBindings,
Doc("Pending elevation request. Read: request summary. Write: 'approve', 'deny', or 'persist'."),
Each("{reqid}", bypassBindings,
Doc("Pending bypass request. Read: request summary. Write: 'approve', 'deny', or 'persist'."),
Bind(BindData),
Read(readElevateRequest),
Write(writeElevateRequest),
Read(readBypassRequest),
Write(writeBypassRequest),
),
),
@ -192,10 +192,10 @@ FileNode("generate", 0666,
Read(readSessionID),
),
FileNode("elevate", 0666,
Doc("Session-scoped elevation policy. Overrides global policy for this session."),
Read(readSessionElevate),
Write(writeSessionElevate),
FileNode("bypass", 0666,
Doc("Session-scoped bypass policy. Overrides global policy for this session."),
Read(readSessionBypass),
Write(writeSessionBypass),
),
DirNode("agent",

View File

@ -111,17 +111,17 @@ Skills are markdown modules that provide specialized domain knowledge, conventio
**Why this matters**: Shell commands produce unstructured text that requires parsing, are sensitive to locale and environment, and compound errors silently. Dedicated tools have typed inputs/outputs, built-in error handling, and consistent behavior. Using them leads to fewer mistakes and more efficient execution.
# Sandbox & Elevation
# Sandbox & Bypass
Tools run in a sandbox with restricted filesystem access. Unexpected permission denied errors are usually caused by sandbox restrictions.
When a tool or shell command fails due to sandbox restrictions, you may retry with `"elevated": true`. This routes the command outside the sandbox through the elevation broker.
When a tool or shell command fails due to sandbox restrictions, you may retry with `"bypass": true`. This routes the command outside the sandbox through the bypass broker.
**Rules**:
- Only use elevation after discovering a sandbox limitation — do not pre-emptively elevate.
- Never nag the user. If an elevated command is denied, move on.
- Maximum three elevation attempts per session. After that, stop trying — the operation cannot proceed.
- Elevation is a call-level flag available on `shell` and all promoted tools: `{"cmd": "...", "elevated": true}`.
- Only use bypass after discovering a sandbox limitation — do not pre-emptively bypass.
- Never nag the user. If a bypassed command is denied, move on.
- Maximum three bypass attempts per session. After that, stop trying — the operation cannot proceed.
- Bypass is a call-level flag available on `shell` and all promoted tools: `{"cmd": "...", "bypass": true}`.
# Security

View File

@ -218,19 +218,19 @@ func (s *Server) callPromotedTool(ctx context.Context, tool string, args json.Ra
}
// Extract dispatch-level flags (not passed to tool script).
elevated := false
bypassed := false
timeout := 30
sandboxName := "default"
var argMap map[string]interface{}
if err := json.Unmarshal(args, &argMap); err == nil {
if e, ok := argMap["elevated"]; ok {
if e, ok := argMap["bypass"]; ok {
switch v := e.(type) {
case bool:
elevated = v
bypassed = v
case string:
elevated = v == "true" || v == "1"
bypassed = v == "true" || v == "1"
}
delete(argMap, "elevated")
delete(argMap, "bypass")
}
if t, ok := argMap["timeout"]; ok {
switch v := t.(type) {
@ -259,7 +259,7 @@ func (s *Server) callPromotedTool(ctx context.Context, tool string, args json.Ra
resolved := m.Resolve()
if resolved != nil && resolved.Sudo {
needsSudo = true
elevated = true // sudo implies elevation
bypassed = true // sudo implies bypass
}
}
@ -273,14 +273,14 @@ func (s *Server) callPromotedTool(ctx context.Context, tool string, args json.Ra
workDir := s.cwd
s.wdMu.RUnlock()
sudoCode := fmt.Sprintf("cat <<'OLLIE_EOF' | %s\n%s\nOLLIE_EOF", code, stdinData)
result, err = s.executeElevatedSudo(ctx, sudoCode, workDir, timeout)
} else if elevated {
result, err = s.executeBypassSudo(ctx, sudoCode, workDir, timeout)
} else if bypassed {
s.wdMu.RLock()
workDir := s.cwd
s.wdMu.RUnlock()
// Broker protocol has no stdin support; pipe JSON via heredoc.
elevatedCode := fmt.Sprintf("cat <<'OLLIE_EOF' | %s\n%s\nOLLIE_EOF", code, stdinData)
result, err = s.executeElevated(ctx, elevatedCode, workDir, timeout)
bypassCode := fmt.Sprintf("cat <<'OLLIE_EOF' | %s\n%s\nOLLIE_EOF", code, stdinData)
result, err = s.executeBypass(ctx, bypassCode, workDir, timeout)
} else {
result, err = s.executeWithStdin(ctx, code, "bash", timeout, sandboxName, false, stdinData)
}

View File

@ -115,20 +115,20 @@ func (lw *limitedWriter) Write(p []byte) (n int, err error) {
}
// Connects to the broker socket, sends the request with the current env,
// and streams the framed response back.
func (s *Server) executeElevated(ctx context.Context, cmd, dir string, timeout int, doDetach ...bool) (string, error) {
return s.executeElevatedOpts(ctx, cmd, dir, timeout, false, doDetach...)
func (s *Server) executeBypass(ctx context.Context, cmd, dir string, timeout int, doDetach ...bool) (string, error) {
return s.executeBypassOpts(ctx, cmd, dir, timeout, false, doDetach...)
}
func (s *Server) executeElevatedSudo(ctx context.Context, cmd, dir string, timeout int) (string, error) {
return s.executeElevatedOpts(ctx, cmd, dir, timeout, true)
func (s *Server) executeBypassSudo(ctx context.Context, cmd, dir string, timeout int) (string, error) {
return s.executeBypassOpts(ctx, cmd, dir, timeout, true)
}
func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeout int, sudo bool, doDetach ...bool) (string, error) {
func (s *Server) executeBypassOpts(ctx context.Context, cmd, dir string, timeout int, sudo bool, doDetach ...bool) (string, error) {
xdg := os.Getenv("XDG_RUNTIME_DIR")
if xdg == "" {
return "", fmt.Errorf("elevation not available: no XDG_RUNTIME_DIR")
return "", fmt.Errorf("bypass not available: no XDG_RUNTIME_DIR")
}
sockPath := filepath.Join(xdg, "ollie", "elevate.sock")
sockPath := filepath.Join(xdg, "ollie", "bypass.sock")
wantDetach := len(doDetach) > 0 && doDetach[0]
@ -143,7 +143,7 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
conn, err := net.DialTimeout("unix", sockPath, 5*time.Second)
if err != nil {
cancel()
return "", fmt.Errorf("elevation not available: %w", err)
return "", fmt.Errorf("bypass not available: %w", err)
}
// Send request — merge process env with session-scoped envExtra.
@ -169,7 +169,7 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
if _, err := conn.Write(reqJSON); err != nil {
conn.Close()
cancel()
return "", fmt.Errorf("elevated execution failed: write: %w", err)
return "", fmt.Errorf("bypass execution failed: write: %w", err)
}
// Set up detach channel
@ -236,7 +236,7 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
select {
case <-detachCh:
// Detach: background the socket read into a goroutine
cmdStr := "elevated: " + cmd
cmdStr := "bypass: " + cmd
if len(cmdStr) > 80 {
cmdStr = cmdStr[:77] + "..."
}
@ -299,14 +299,14 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
combined := outputBuf.String()
if fr.exitCode != 0 {
if combined == "" {
return "", fmt.Errorf("elevated execution failed (exit %d)", fr.exitCode)
return "", fmt.Errorf("bypass execution failed (exit %d)", fr.exitCode)
}
errOutput := combined
const maxErrOutput = 8192
if len(errOutput) > maxErrOutput {
errOutput = errOutput[:maxErrOutput] + fmt.Sprintf("\n[...truncated %d bytes in error]", len(combined)-maxErrOutput)
}
return combined, fmt.Errorf("elevated execution failed (exit %d)\nOutput: %s", fr.exitCode, errOutput)
return combined, fmt.Errorf("bypass execution failed (exit %d)\nOutput: %s", fr.exitCode, errOutput)
}
return combined, nil
@ -316,13 +316,13 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
cancel()
combined := outputBuf.String()
if ctx.Err() == context.DeadlineExceeded {
return combined, fmt.Errorf("elevated execution timeout after %d seconds", timeout)
return combined, fmt.Errorf("bypass execution timeout after %d seconds", timeout)
}
return combined, fmt.Errorf("elevated execution interrupted")
return combined, fmt.Errorf("bypass execution interrupted")
case <-detachCh:
// Manual detach: background the connection read
cmdStr := "elevated: " + cmd
cmdStr := "bypass: " + cmd
if len(cmdStr) > 80 {
cmdStr = cmdStr[:77] + "..."
}