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

123 lines
2.6 KiB
Go

// Package bypass handles bypass request submission and resolution for toolsrvclient.
// 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
cancelled bool
}
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 markCancelled(req *Request) {
mu.Lock()
if current, ok := requests[req.ID]; ok && current == req {
delete(requests, req.ID)
req.cancelled = true
}
mu.Unlock()
}
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()
select {
case pending <- req:
case <-ctx.Done():
markCancelled(req)
return false, ctx.Err()
}
select {
case <-req.done:
case <-ctx.Done():
markCancelled(req)
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.
// Returns nil if context is cancelled before a request arrives.
func NextPending(ctx context.Context) *Request {
for {
select {
case req := <-pending:
mu.Lock()
_, active := requests[req.ID]
mu.Unlock()
if active {
return req
}
case <-ctx.Done():
return nil
}
}
}
// 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()
defer mu.Unlock()
req, ok := requests[id]
if !ok {
return false
}
delete(requests, id)
req.approved = approved
if errMsg != "" {
req.err = errors.New(errMsg)
}
close(req.done)
return true
}
func nextID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}