ollie/cmd/toolsrv/internal/bypass/bypass.go

113 lines
2.4 KiB
Go

// Package bypass handles bypass request submission and resolution for toolsrv.
// Bypass requests are submitted by sandboxed execution, exposed via 9P for
// external approval (olliesrv), and resolved when the approver responds.
package bypass
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"sync"
)
// Request represents a bypass request.
type Request struct {
ID string `json:"id"`
Cmd string `json:"cmd"`
Cwd string `json:"cwd"`
Env map[string]string `json:"env,omitempty"`
// Resolution state (not serialized)
done chan struct{}
approved bool
err error
}
var (
pending = make(chan *Request, 16)
mu sync.Mutex
requests = make(map[string]*Request)
)
// Submit submits a bypass request and blocks until resolved or context cancelled.
// Returns (approved, error). If denied, approved is false and error is nil.
// If context is cancelled, returns (false, ctx.Err()).
func Submit(ctx context.Context, cmd, cwd string, env map[string]string) (bool, error) {
req := &Request{
ID: nextID(),
Cmd: cmd,
Cwd: cwd,
Env: env,
done: make(chan struct{}),
}
mu.Lock()
requests[req.ID] = req
mu.Unlock()
// Send to pending channel (may block if channel is full)
select {
case pending <- req:
case <-ctx.Done():
mu.Lock()
delete(requests, req.ID)
mu.Unlock()
return false, ctx.Err()
}
// Wait for resolution or context cancellation
select {
case <-req.done:
case <-ctx.Done():
mu.Lock()
delete(requests, req.ID)
mu.Unlock()
return false, ctx.Err()
}
mu.Lock()
delete(requests, req.ID)
mu.Unlock()
return req.approved, req.err
}
// NextPending blocks until a request is available and returns it.
// Used by the 9P pending file to expose requests to the approver.
func NextPending() *Request {
return <-pending
}
// Resolve completes a pending request with the given decision.
// Returns false if the request ID is not found (already resolved or invalid).
func Resolve(id string, approved bool, errMsg string) bool {
mu.Lock()
req, ok := requests[id]
mu.Unlock()
if !ok {
return false
}
req.approved = approved
if errMsg != "" {
req.err = errors.New(errMsg)
}
close(req.done)
return true
}
// PendingCount returns the number of pending requests (for diagnostics).
func PendingCount() int {
mu.Lock()
defer mu.Unlock()
return len(requests)
}
func nextID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}