// 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) }