diff --git a/README.md b/README.md index 620661b..5b1c73c 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/elevate/broker.go b/bypass/broker.go similarity index 99% rename from elevate/broker.go rename to bypass/broker.go index 3268e1d..39336b6 100644 --- a/elevate/broker.go +++ b/bypass/broker.go @@ -1,4 +1,4 @@ -package elevate +package bypass import ( "encoding/binary" diff --git a/elevate/broker_test.go b/bypass/broker_test.go similarity index 99% rename from elevate/broker_test.go rename to bypass/broker_test.go index 2bedf81..f65d5e3 100644 --- a/elevate/broker_test.go +++ b/bypass/broker_test.go @@ -1,4 +1,4 @@ -package elevate +package bypass import ( "encoding/binary" diff --git a/elevate/policy.go b/bypass/policy.go similarity index 99% rename from elevate/policy.go rename to bypass/policy.go index c166069..7c864f4 100644 --- a/elevate/policy.go +++ b/bypass/policy.go @@ -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" diff --git a/elevate/policy_test.go b/bypass/policy_test.go similarity index 99% rename from elevate/policy_test.go rename to bypass/policy_test.go index df5a134..927bdaa 100644 --- a/elevate/policy_test.go +++ b/bypass/policy_test.go @@ -1,4 +1,4 @@ -package elevate +package bypass import ( "os" diff --git a/elevate/request.go b/bypass/request.go similarity index 98% rename from elevate/request.go rename to bypass/request.go index fcc3faa..062be21 100644 --- a/elevate/request.go +++ b/bypass/request.go @@ -1,4 +1,4 @@ -package elevate +package bypass import ( "fmt" diff --git a/cmd/olliesrv/elevate_notify.go b/cmd/olliesrv/bypass_notify.go similarity index 79% rename from cmd/olliesrv/elevate_notify.go rename to cmd/olliesrv/bypass_notify.go index 5525824..c025f8e 100644 --- a/cmd/olliesrv/elevate_notify.go +++ b/cmd/olliesrv/bypass_notify.go @@ -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) } } } diff --git a/cmd/olliesrv/main.go b/cmd/olliesrv/main.go index e24e8c5..a1eb4f2 100644 --- a/cmd/olliesrv/main.go +++ b/cmd/olliesrv/main.go @@ -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) } } } diff --git a/data/agents/README.md b/data/agents/README.md index 4677644..aab6dda 100644 --- a/data/agents/README.md +++ b/data/agents/README.md @@ -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 | diff --git a/data/scripts/o b/data/scripts/o index 7dd1ce6..75bf5ce 100755 --- a/data/scripts/o +++ b/data/scripts/o @@ -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" diff --git a/data/tools/shell b/data/tools/shell index 04d9dbc..5f6d27b 100755 --- a/data/tools/shell +++ b/data/tools/shell @@ -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. diff --git a/data/tools/shell.meta b/data/tools/shell.meta index a5c3c5b..f9ad19d 100644 --- a/data/tools/shell.meta +++ b/data/tools/shell.meta @@ -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." } } }, diff --git a/doc/resources/architecture.md b/doc/resources/architecture.md index 5a89041..740c860 100644 --- a/doc/resources/architecture.md +++ b/doc/resources/architecture.md @@ -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}/ diff --git a/doc/resources/core.md b/doc/resources/core.md index 33841b8..b11756b 100644 --- a/doc/resources/core.md +++ b/doc/resources/core.md @@ -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 diff --git a/doc/resources/edsl.md b/doc/resources/edsl.md index 14581bc..ab7bc09 100644 --- a/doc/resources/edsl.md +++ b/doc/resources/edsl.md @@ -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() diff --git a/doc/resources/evolution.md b/doc/resources/evolution.md index bc78c70..2ee6616 100644 --- a/doc/resources/evolution.md +++ b/doc/resources/evolution.md @@ -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 ``` diff --git a/doc/resources/misc.md b/doc/resources/misc.md index 741e44c..9f13c55 100644 --- a/doc/resources/misc.md +++ b/doc/resources/misc.md @@ -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 diff --git a/doc/resources/no-mcp.md b/doc/resources/no-mcp.md index 1c00833..40cf8fe 100644 --- a/doc/resources/no-mcp.md +++ b/doc/resources/no-mcp.md @@ -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 diff --git a/doc/resources/remote-execution.md b/doc/resources/remote-execution.md index e09bd31..e0d4b7b 100644 --- a/doc/resources/remote-execution.md +++ b/doc/resources/remote-execution.md @@ -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. diff --git a/doc/resources/tool-registry.md b/doc/resources/tool-registry.md index f769814..dcc1d10 100644 --- a/doc/resources/tool-registry.md +++ b/doc/resources/tool-registry.md @@ -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. diff --git a/doc/resources/writing-tools.md b/doc/resources/writing-tools.md index b27b885..bad8c42 100644 --- a/doc/resources/writing-tools.md +++ b/doc/resources/writing-tools.md @@ -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 diff --git a/fs/handlers.go b/fs/handlers.go index 56e5227..5d749b8 100644 --- a/fs/handlers.go +++ b/fs/handlers.go @@ -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 } diff --git a/fs/newroot.go b/fs/newroot.go index 4cc4fce..12f2863 100644 --- a/fs/newroot.go +++ b/fs/newroot.go @@ -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, } diff --git a/fs/spec.go b/fs/spec.go index 3e882a1..78d0ce7 100644 --- a/fs/spec.go +++ b/fs/spec.go @@ -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", diff --git a/prompts/system_prompt.md b/prompts/system_prompt.md index 8661854..227785f 100644 --- a/prompts/system_prompt.md +++ b/prompts/system_prompt.md @@ -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 diff --git a/toolsrv/server.go b/toolsrv/server.go index 3d42066..dd30d50 100644 --- a/toolsrv/server.go +++ b/toolsrv/server.go @@ -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) } diff --git a/toolsrv/shell.go b/toolsrv/shell.go index c26554e..e216a29 100644 --- a/toolsrv/shell.go +++ b/toolsrv/shell.go @@ -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] + "..." }