From 5f052c1db074f0e795a36552e5856923e0f38e9e Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 4 Oct 2026 14:36:43 +0800 Subject: [PATCH] feat(agent): select isolated real adapter without mock fallback --- internal/rpc/approved_execution.go | 18 +++++++++++----- internal/rpc/approved_execution_test.go | 28 +++++++++++++++++++++++++ internal/rpc/server.go | 8 +++++-- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/internal/rpc/approved_execution.go b/internal/rpc/approved_execution.go index 1257320..75d1b66 100644 --- a/internal/rpc/approved_execution.go +++ b/internal/rpc/approved_execution.go @@ -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 diff --git a/internal/rpc/approved_execution_test.go b/internal/rpc/approved_execution_test.go index fa2d2d9..8de6305 100644 --- a/internal/rpc/approved_execution_test.go +++ b/internal/rpc/approved_execution_test.go @@ -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) diff --git a/internal/rpc/server.go b/internal/rpc/server.go index 4392d57..66672b2 100644 --- a/internal/rpc/server.go +++ b/internal/rpc/server.go @@ -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), }