package service import ( "context" "errors" "time" toki "git.toki-labs.com/toki/proto-socket/go" edgenode "iop/apps/edge/internal/node" "iop/packages/go/config" "iop/packages/go/workspaceprotocol" iop "iop/proto/gen/iop" ) var ( errWorkspaceWireUnavailable = errors.New("workspace wire: admitted node is unavailable") errWorkspaceWireStale = errors.New("workspace wire: admitted node connection changed") errWorkspaceWireTransport = errors.New("workspace wire: request failed") errWorkspaceWireReference = errors.New("workspace wire: workspace reference is not admitted") errWorkspaceWireResponse = errors.New("workspace wire: node response was not accepted") errWorkspaceWireArtifact = errors.New("workspace wire: artifact request was not accepted") ) // workspaceOpen sends only to the Node and connection generation frozen by // workspace admission. It never resolves a replacement Node after reconnect, and // it rejects any request whose workspace reference is not the exact admitted // reference before dispatching to the Node. func (s *Service) workspaceOpen(ctx context.Context, binding *SingleRequestWorkspaceBinding, req *iop.WorkspaceOpenRequest) (*iop.WorkspaceOpenResponse, error) { if req == nil || req.GetRequestId() == "" { return nil, errWorkspaceWireUnavailable } if binding == nil { return nil, errWorkspaceWireUnavailable } if req.GetWorkspaceRef() == "" || req.GetWorkspaceRef() != binding.Ref { return nil, errWorkspaceWireReference } outbound, err := workspaceOpenRequestFromBinding(req, binding) if err != nil { return nil, errWorkspaceWireUnavailable } wait := workspaceWireTimeout(ctx, binding, outbound.GetTimeoutMs()) var response *iop.WorkspaceOpenResponse err = s.withWorkspaceBinding(binding, func(entry *edgenode.NodeEntry) error { var err error response, err = toki.SendRequestTyped[*iop.WorkspaceOpenRequest, *iop.WorkspaceOpenResponse](&entry.Client.Communicator, outbound, wait) return err }) if err != nil { return nil, workspaceWireError(err) } return validateWorkspaceOpenResponse(outbound, response) } func workspaceOpenRequestFromBinding(req *iop.WorkspaceOpenRequest, binding *SingleRequestWorkspaceBinding) (*iop.WorkspaceOpenRequest, error) { frozen, err := validateAndCloneWorkspaceBinding(binding) if err != nil { return nil, err } operations := make([]iop.WorkspaceOperation, 0, len(frozen.OperationIDs)) var readEnabled, listEnabled, writeEnabled, commandEnabled bool for _, operationID := range frozen.OperationIDs { switch config.WorkspaceOperation(operationID) { case config.WorkspaceOpRead: operations = append(operations, iop.WorkspaceOperation_WORKSPACE_OPERATION_READ) readEnabled = true case config.WorkspaceOpList: operations = append(operations, iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST) listEnabled = true case config.WorkspaceOpWrite: operations = append(operations, iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE) writeEnabled = true case config.WorkspaceOpDelete: operations = append(operations, iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE) case config.WorkspaceOpCommand: operations = append(operations, iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND) commandEnabled = true default: return nil, errSingleRequestWorkspaceMalformed } } outbound := &iop.WorkspaceOpenRequest{ RequestId: req.GetRequestId(), WorkspaceRef: frozen.Ref, TimeoutMs: req.GetTimeoutMs(), Operations: operations, CommandIds: append([]string(nil), frozen.CommandIDs...), } if readEnabled { outbound.MaxReadBytes = int64(frozen.Limits.MaxReadBytes) } if writeEnabled { outbound.MaxWriteBytes = int64(frozen.Limits.MaxWriteBytes) } if listEnabled || commandEnabled { outbound.MaxOutputBytes = int64(frozen.Limits.MaxOutputBytes) } if commandEnabled { outbound.MaxCommandTimeoutMs = int64(frozen.Limits.MaxCommandTimeoutMS) } return outbound, nil } // workspaceTool uses a bounded waiter and sends one typed cancel for an // in-flight tool call when its context is cancelled. The immutable request, // stage, and tool-call identities are copied to that cancellation request. // // The admitted Node communicator is captured once up front. When the caller // context wins the race against the tool response, cancellation is a single // fire-and-forget typed send to that captured communicator: it never acquires // the registry dispatch-owner mutex the in-flight tool request is holding, never // re-selects a Node, and does not wait for a cancel response. func (s *Service) workspaceTool(ctx context.Context, binding *SingleRequestWorkspaceBinding, req *iop.WorkspaceToolRequest) (*iop.WorkspaceToolResponse, error) { if req == nil || req.GetRequestId() == "" || req.GetStageId() == "" || req.GetToolCallId() == "" { return nil, errWorkspaceWireUnavailable } client, err := s.captureWorkspaceClient(binding) if err != nil { return nil, err } type result struct { response *iop.WorkspaceToolResponse err error } resultCh := make(chan result, 1) wait := workspaceWireTimeout(ctx, binding, req.GetTimeoutMs()) go func() { var response *iop.WorkspaceToolResponse err := s.withWorkspaceBinding(binding, func(entry *edgenode.NodeEntry) error { var requestErr error response, requestErr = toki.SendRequestTyped[*iop.WorkspaceToolRequest, *iop.WorkspaceToolResponse](&entry.Client.Communicator, req, wait) return requestErr }) resultCh <- result{response: response, err: err} }() select { case result := <-resultCh: if result.err != nil { return nil, workspaceWireError(result.err) } return validateWorkspaceToolResponse(req, result.response) case <-ctx.Done(): s.sendWorkspaceCancelToClient(client, binding, req) return nil, context.Cause(ctx) } } // workspaceArtifact dispatches the coordinator-only PLAN/REVIEW artifact // family to the frozen Node generation. Both request and response payloads are // bounded by the admitted output limit before they can cross their respective // trust boundaries. func (s *Service) workspaceArtifact(ctx context.Context, binding *SingleRequestWorkspaceBinding, req *iop.WorkspaceArtifactRequest, maxBytes int) (*iop.WorkspaceArtifactResponse, error) { if req == nil || req.GetRequestId() == "" || binding == nil || maxBytes < 1 || !validWorkspaceArtifactKind(req.GetKind()) || !validWorkspaceArtifactOperation(req.GetOperation()) { return nil, errWorkspaceWireArtifact } limit := maxBytes if (req.GetOperation() == iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ && len(req.GetContent()) != 0) || len(req.GetContent()) > limit { return nil, errWorkspaceWireArtifact } outbound := &iop.WorkspaceArtifactRequest{ RequestId: req.GetRequestId(), Kind: req.GetKind(), Operation: req.GetOperation(), Content: append([]byte(nil), req.GetContent()...), } wait := workspaceWireTimeout(ctx, binding, 0) var response *iop.WorkspaceArtifactResponse err := s.withWorkspaceBinding(binding, func(entry *edgenode.NodeEntry) error { var requestErr error response, requestErr = toki.SendRequestTyped[*iop.WorkspaceArtifactRequest, *iop.WorkspaceArtifactResponse](&entry.Client.Communicator, outbound, wait) return requestErr }) if err != nil { return nil, workspaceWireError(err) } return validateWorkspaceArtifactResponse(outbound, response, limit) } // sendWorkspaceCancelToClient issues exactly one fire-and-forget typed cancel to // the captured admitted communicator, copying the immutable request/stage/tool // identities. It runs in its own goroutine because the caller has already // returned on context cancellation; it never resolves a replacement Node, never // takes the registry lock, and ignores the cancel response. func (s *Service) sendWorkspaceCancelToClient(client *toki.TcpClient, binding *SingleRequestWorkspaceBinding, req *iop.WorkspaceToolRequest) { if client == nil { return } cancel := &iop.WorkspaceCancelRequest{RequestId: req.GetRequestId(), StageId: req.GetStageId(), ToolCallId: req.GetToolCallId()} wait := workspaceWireTimeout(context.Background(), binding, 0) go func() { _, _ = toki.SendRequestTyped[*iop.WorkspaceCancelRequest, *iop.WorkspaceCancelResponse](&client.Communicator, cancel, wait) }() } // captureWorkspaceClient snapshots the admitted Node's communicator and confirms // its exact connection generation before any tool dispatch. The captured client // is the only Node a later cancellation may target. func (s *Service) captureWorkspaceClient(binding *SingleRequestWorkspaceBinding) (*toki.TcpClient, error) { if binding == nil || binding.NodeID == "" || binding.ConnectionGeneration == 0 || s == nil || s.registry == nil { return nil, errWorkspaceWireUnavailable } entry, ok := s.registry.ReadyOwnerSnapshot(binding.NodeID) if !ok || entry == nil || entry.Client == nil { return nil, errWorkspaceWireUnavailable } if entry.ConnectionGeneration != binding.ConnectionGeneration { return nil, errWorkspaceWireStale } return entry.Client, nil } func (s *Service) workspaceCancel(ctx context.Context, binding *SingleRequestWorkspaceBinding, req *iop.WorkspaceCancelRequest) (*iop.WorkspaceCancelResponse, error) { if req == nil || req.GetRequestId() == "" || req.GetStageId() == "" || req.GetToolCallId() == "" { return nil, errWorkspaceWireUnavailable } wait := workspaceWireTimeout(ctx, binding, 0) var response *iop.WorkspaceCancelResponse err := s.withWorkspaceBinding(binding, func(entry *edgenode.NodeEntry) error { var err error response, err = toki.SendRequestTyped[*iop.WorkspaceCancelRequest, *iop.WorkspaceCancelResponse](&entry.Client.Communicator, req, wait) return err }) if err != nil { return nil, workspaceWireError(err) } return validateWorkspaceCancelResponse(req, response) } func (s *Service) workspaceCleanup(ctx context.Context, binding *SingleRequestWorkspaceBinding, req *iop.WorkspaceCleanupRequest) (*iop.WorkspaceCleanupResponse, error) { if req == nil || req.GetRequestId() == "" { return nil, errWorkspaceWireUnavailable } wait := workspaceWireTimeout(ctx, binding, 0) var response *iop.WorkspaceCleanupResponse err := s.withWorkspaceBinding(binding, func(entry *edgenode.NodeEntry) error { var err error response, err = toki.SendRequestTyped[*iop.WorkspaceCleanupRequest, *iop.WorkspaceCleanupResponse](&entry.Client.Communicator, req, wait) return err }) if err != nil { return nil, workspaceWireError(err) } return validateWorkspaceCleanupResponse(req, response) } // The response validators enforce the immutable coordinator identity echoes and // the exact canonical status, error-code, and generic message triples for each // operation using workspaceprotocol authority. Mismatched identities, // non-canonical status/code pairs, raw text leakage, or nil responses are // translated to errWorkspaceWireResponse. They never return, log, or // interpolate raw Node-supplied text. func validateWorkspaceOpenResponse(req *iop.WorkspaceOpenRequest, resp *iop.WorkspaceOpenResponse) (*iop.WorkspaceOpenResponse, error) { if resp == nil || resp.GetRequestId() != req.GetRequestId() || resp.GetWorkspaceRef() != req.GetWorkspaceRef() { return nil, errWorkspaceWireResponse } expectedErr, ok := workspaceprotocol.OpenTerminal(resp.GetStatus(), resp.GetErrorCode()) if !ok || resp.GetError() != expectedErr { return nil, errWorkspaceWireResponse } return resp, nil } func validateWorkspaceToolResponse(req *iop.WorkspaceToolRequest, resp *iop.WorkspaceToolResponse) (*iop.WorkspaceToolResponse, error) { if resp == nil || resp.GetRequestId() != req.GetRequestId() || resp.GetStageId() != req.GetStageId() || resp.GetToolCallId() != req.GetToolCallId() { return nil, errWorkspaceWireResponse } expectedErr, ok := workspaceprotocol.ToolTerminal(resp.GetStatus(), resp.GetErrorCode()) if !ok || resp.GetError() != expectedErr { return nil, errWorkspaceWireResponse } return resp, nil } func validateWorkspaceArtifactResponse(req *iop.WorkspaceArtifactRequest, resp *iop.WorkspaceArtifactResponse, limit int) (*iop.WorkspaceArtifactResponse, error) { if resp == nil || resp.GetRequestId() != req.GetRequestId() || resp.GetKind() != req.GetKind() || resp.GetOperation() != req.GetOperation() { return nil, errWorkspaceWireResponse } expectedErr, ok := workspaceprotocol.ArtifactTerminal(resp.GetStatus(), resp.GetErrorCode()) if !ok || resp.GetError() != expectedErr || len(resp.GetContent()) > limit { return nil, errWorkspaceWireResponse } if (resp.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS && len(resp.GetContent()) != 0) || (resp.GetOperation() == iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE && len(resp.GetContent()) != 0) { return nil, errWorkspaceWireResponse } return resp, nil } func validWorkspaceArtifactKind(kind iop.WorkspaceArtifactKind) bool { return kind == iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN || kind == iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW } func validWorkspaceArtifactOperation(operation iop.WorkspaceArtifactOperation) bool { return operation == iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ || operation == iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE } func validateWorkspaceCancelResponse(req *iop.WorkspaceCancelRequest, resp *iop.WorkspaceCancelResponse) (*iop.WorkspaceCancelResponse, error) { if resp == nil || resp.GetRequestId() != req.GetRequestId() || resp.GetStageId() != req.GetStageId() || resp.GetToolCallId() != req.GetToolCallId() { return nil, errWorkspaceWireResponse } expectedErr, ok := workspaceprotocol.CancelTerminal(resp.GetStatus(), resp.GetErrorCode()) if !ok || resp.GetError() != expectedErr { return nil, errWorkspaceWireResponse } return resp, nil } func validateWorkspaceCleanupResponse(req *iop.WorkspaceCleanupRequest, resp *iop.WorkspaceCleanupResponse) (*iop.WorkspaceCleanupResponse, error) { if resp == nil || resp.GetRequestId() != req.GetRequestId() { return nil, errWorkspaceWireResponse } expectedErr, ok := workspaceprotocol.CleanupTerminal(resp.GetStatus(), resp.GetErrorCode()) if !ok || resp.GetError() != expectedErr { return nil, errWorkspaceWireResponse } return resp, nil } func (s *Service) withWorkspaceBinding(binding *SingleRequestWorkspaceBinding, send func(*edgenode.NodeEntry) error) error { if binding == nil || binding.NodeID == "" || binding.ConnectionGeneration == 0 || s == nil || s.registry == nil { return errWorkspaceWireUnavailable } entry, ok := s.registry.ReadyOwnerSnapshot(binding.NodeID) if !ok || entry == nil || entry.Client == nil { return errWorkspaceWireUnavailable } if entry.ConnectionGeneration != binding.ConnectionGeneration { return errWorkspaceWireStale } if err := s.registry.WithCurrentDispatchOwner(binding.NodeID, entry.Client, binding.ConnectionGeneration, func() error { return send(entry) }); err != nil { return errWorkspaceWireStale } return nil } func workspaceWireTimeout(ctx context.Context, binding *SingleRequestWorkspaceBinding, requestMS int64) time.Duration { wait := 30 * time.Second if binding != nil && binding.Limits.MaxCommandTimeoutMS > 0 { wait = time.Duration(binding.Limits.MaxCommandTimeoutMS) * time.Millisecond } if requestMS > 0 { requestWait := time.Duration(requestMS) * time.Millisecond if requestWait < wait { wait = requestWait } } if deadline, ok := ctx.Deadline(); ok { if remaining := time.Until(deadline); remaining < wait { wait = remaining } } if wait <= 0 { return time.Millisecond } return wait } func workspaceWireError(err error) error { if errors.Is(err, errWorkspaceWireUnavailable) || errors.Is(err, errWorkspaceWireStale) { return err } return errWorkspaceWireTransport }