feat(agent): select isolated real adapter without mock fallback
This commit is contained in:
@@ -72,7 +72,7 @@ func (s *Server) GetLoadedSIP(ctx context.Context, req *agentpb.GetLoadedSIPRequ
|
||||
|
||||
// ExecuteApproved is mock-only until real Agent/Asterisk loading and supplier
|
||||
// contracts have been separately verified. It persists a one-shot unknown
|
||||
// execution BEFORE calling the mock adapter. Ambiguous attempts never redial.
|
||||
// execution BEFORE calling the selected adapter. Ambiguous attempts never redial.
|
||||
func (s *Server) ExecuteApproved(ctx context.Context, req *agentpb.ExecuteApprovedRequest) (*agentpb.ExecuteApprovedResponse, error) {
|
||||
if req == nil || req.Meta == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "approved execution metadata is required")
|
||||
@@ -87,8 +87,16 @@ func (s *Server) ExecuteApproved(ctx context.Context, req *agentpb.ExecuteApprov
|
||||
if req.DispatcherId != dispatcherID {
|
||||
return nil, status.Error(codes.PermissionDenied, "Dispatcher does not own the active Agent session")
|
||||
}
|
||||
if s.mode != "mock" || s.mockApprovedOriginate == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "approved origination requires the isolated mock adapter")
|
||||
originate := s.mockApprovedOriginate
|
||||
switch s.mode {
|
||||
case "mock":
|
||||
case "nonprod-real":
|
||||
originate = s.nonprodRealOriginate
|
||||
default:
|
||||
return nil, status.Error(codes.FailedPrecondition, "approved origination mode is disabled")
|
||||
}
|
||||
if originate == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "approved origination adapter is unavailable")
|
||||
}
|
||||
const maxTimeoutMS = int64(math.MaxInt64 / int64(time.Millisecond))
|
||||
if req.TenantId <= 0 || req.TaskId == "" || req.SourceEventId == "" || req.CallId == "" || req.SelectedTrunkId == "" || req.CallerId == "" || req.Callee == "" || req.DialedCallee == "" || req.RingTimeoutMs <= 0 || req.RingTimeoutMs > maxTimeoutMS || req.MaxCallDurationMs <= 0 || req.MaxCallDurationMs > maxTimeoutMS || req.SipRevision <= 0 || req.Meta.IdempotencyKey != req.SourceEventId {
|
||||
@@ -139,7 +147,7 @@ func (s *Server) ExecuteApproved(ctx context.Context, req *agentpb.ExecuteApprov
|
||||
MaxCallDuration: time.Duration(req.MaxCallDurationMs) * time.Millisecond,
|
||||
DialBefore: time.UnixMilli(req.DialBeforeUnixMs), AI: bound,
|
||||
}
|
||||
if err := s.mockApprovedOriginate(ctx, approved); err != nil {
|
||||
if err := originate(ctx, approved); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrApprovedDialExpired):
|
||||
log.Printf("Agent approved execution refused before dial: event_id=%q task_id=%q cause_type=%T", req.SourceEventId, req.TaskId, err)
|
||||
@@ -149,7 +157,7 @@ func (s *Server) ExecuteApproved(ctx context.Context, req *agentpb.ExecuteApprov
|
||||
return nil, status.Error(codes.FailedPrecondition, "approved call was not ready before dial")
|
||||
default:
|
||||
log.Printf("Agent approved execution outcome unknown: event_id=%q task_id=%q cause_type=%T", req.SourceEventId, req.TaskId, err)
|
||||
return nil, status.Error(codes.Unavailable, "approved mock execution outcome unknown")
|
||||
return nil, status.Error(codes.Unavailable, "approved execution outcome unknown")
|
||||
}
|
||||
}
|
||||
return &agentpb.ExecuteApprovedResponse{CallId: req.CallId, Accepted: true}, nil
|
||||
|
||||
@@ -77,6 +77,34 @@ func activatedApprovedServer(t *testing.T, now time.Time, statePath string, disp
|
||||
return s
|
||||
}
|
||||
|
||||
func TestExecuteApprovedRealModeNeverUsesMockAdapter(t *testing.T) {
|
||||
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
|
||||
req := approvedTestRequest(t, now)
|
||||
s := activatedApprovedServer(t, now, filepath.Join(t.TempDir(), "agent-session.json"), req.DispatcherId, 1, func(context.Context, ApprovedExecution) error {
|
||||
t.Fatal("real mode used mock adapter")
|
||||
return nil
|
||||
})
|
||||
s.mode = "nonprod-real"
|
||||
if _, err := s.ExecuteApproved(context.Background(), req); status.Code(err) != codes.FailedPrecondition {
|
||||
t.Fatalf("real mode without its own adapter must refuse before journaling: %v", err)
|
||||
}
|
||||
attempts := 0
|
||||
s.nonprodRealOriginate = func(_ context.Context, approved ApprovedExecution) error {
|
||||
attempts++
|
||||
if approved.DialedCallee != req.DialedCallee || approved.CallerID != req.CallerId || approved.SelectedTrunkID != req.SelectedTrunkId {
|
||||
t.Fatal("signed real dial identity was altered")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
response, err := s.ExecuteApproved(context.Background(), req)
|
||||
if err != nil || response == nil || !response.Accepted || attempts != 1 {
|
||||
t.Fatalf("real mode should dispatch exactly once through its adapter: response=%v err=%v attempts=%d", response, err, attempts)
|
||||
}
|
||||
if _, err := s.ExecuteApproved(context.Background(), req); status.Code(err) != codes.FailedPrecondition || attempts != 1 {
|
||||
t.Fatalf("real mode must not redial an accepted identity: err=%v attempts=%d", err, attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteApprovedBindsConfigAndPreventsRedialAcrossRestart(t *testing.T) {
|
||||
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
|
||||
req := approvedTestRequest(t, now)
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// ServerOptions contains deployment-bound identity and mock policy inputs.
|
||||
// Production credentials are supplied to grpc.Server through TLS credentials.
|
||||
// ServerOptions contains deployment-bound identity and call policy inputs.
|
||||
// Agent credentials are supplied to grpc.Server through TLS credentials.
|
||||
type ServerOptions struct {
|
||||
Mode string
|
||||
Status *agentpb.AgentStatus
|
||||
@@ -38,6 +38,8 @@ type ServerOptions struct {
|
||||
ApplySIP func(context.Context, []byte) (map[string]int64, error)
|
||||
// The mock may have issued a call even if its outcome is unknown.
|
||||
MockApprovedOriginate func(context.Context, ApprovedExecution) error
|
||||
// NonprodRealOriginate is never selected by the isolated Mock mode.
|
||||
NonprodRealOriginate func(context.Context, ApprovedExecution) error
|
||||
// ApprovedTaskCalls is shared with the approved call runner; nil rejects task controls.
|
||||
ApprovedTaskCalls *agent.TaskCalls
|
||||
}
|
||||
@@ -58,6 +60,7 @@ type Server struct {
|
||||
applySIP func(context.Context, []byte) (map[string]int64, error)
|
||||
sipMu sync.Mutex
|
||||
mockApprovedOriginate func(context.Context, ApprovedExecution) error
|
||||
nonprodRealOriginate func(context.Context, ApprovedExecution) error
|
||||
approvedTaskCalls *agent.TaskCalls
|
||||
sessions *SessionRegistry
|
||||
|
||||
@@ -93,6 +96,7 @@ func NewServer(options ServerOptions) *Server {
|
||||
loadedSIP: options.LoadedSIP,
|
||||
applySIP: options.ApplySIP,
|
||||
mockApprovedOriginate: options.MockApprovedOriginate,
|
||||
nonprodRealOriginate: options.NonprodRealOriginate,
|
||||
approvedTaskCalls: options.ApprovedTaskCalls,
|
||||
sessions: NewSessionRegistry(options.StatePath),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user