package main import ( "context" "errors" "log" "os" "path/filepath" "reflect" "strings" "time" agentpb "git.ipao.vip/rogee/go-sip/gen/agent" "git.ipao.vip/rogee/go-sip/internal/agent" "git.ipao.vip/rogee/go-sip/internal/asterisk" "git.ipao.vip/rogee/go-sip/internal/config" "git.ipao.vip/rogee/go-sip/internal/rpc" "git.ipao.vip/rogee/go-sip/internal/tenant" "github.com/google/uuid" ) func reportDefiniteNonDialFailure(ctx context.Context, cause error, client agent.RecordingClient) error { if !asterisk.NativeCallNeverSubmitted(cause) { return nil // ARI submission may have happened: keep the unknown reservation } endCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) defer cancel() return client.ReportEnded(endCtx) } // newRealAgentServer only exposes the signed Dispatcher execution path. There // is no scenario, synthetic recording, local dial entry, or Mock upload host. func newRealAgentServer(ctx context.Context, settings config.AgentEnvironment, dispatcher agentpb.AgentControlServiceClient, mirror *asterisk.HEPMirror) (*rpc.Server, error) { if ctx == nil || ctx.Err() != nil || settings.Mode != "nonprod-real" || settings.AgentID == "" || settings.CellID == "" || settings.SessionPath == "" || settings.RecoveryRoot == "" || len(settings.PeerFingerprints) == 0 || settings.OSSAllowedHost == "" || dispatcher == nil || mirror == nil || tenant.ValidateDispatcherID(settings.DispatcherID) != nil { return nil, errors.New("real Agent requires approved nonproduction identity, pinned Dispatcher and private recovery") } value := reflect.ValueOf(dispatcher) if value.Kind() == reflect.Pointer && value.IsNil() { return nil, errors.New("real Agent requires a live pinned Dispatcher transport") } if err := settings.ValidateCapture(); err != nil { return nil, err } for _, path := range []string{settings.AsteriskConfigDir, settings.AsteriskBin, settings.AsteriskLibraryDir, settings.RecoveryRoot} { if !filepath.IsAbs(path) { return nil, errors.New("real Agent paths must be absolute") } } if strings.ContainsAny(settings.OSSAllowedHost, "/@ ") || settings.MockScenarioFile != "" || settings.MockAppliedSIPFile != "" { return nil, errors.New("real Agent must not use Mock assets or an invalid OSS host") } root, err := os.Stat(settings.RecoveryRoot) if err != nil || !root.IsDir() || root.Mode().Perm() != 0700 { return nil, errors.New("real Agent recovery directory must exist with mode 0700") } if err := rejectLegacyAgentSpool(settings.RecoveryRoot); err != nil { return nil, err } if _, err := os.Lstat(filepath.Join(settings.RecoveryRoot, ".executions")); err == nil { return nil, errors.New("legacy Agent execution state requires operator disposition") } else if !errors.Is(err, os.ErrNotExist) { return nil, err } loader := asterisk.Loader{ConfigDir: settings.AsteriskConfigDir, Asterisk: settings.AsteriskBin, LibraryDir: settings.AsteriskLibraryDir} var handler *rpc.Server recordingClient := func(execution rpc.ApprovedExecution) agent.RecordingClient { return agent.RecordingClient{ Client: dispatcher, DispatcherID: execution.DispatcherID, TenantID: execution.TenantID, SourceEventID: execution.SourceEventID, Session: func(context.Context) (*agentpb.RequestMeta, error) { return handler.ActiveSessionMeta() }, } } worker := &rpc.ApprovedCallWorker{ Lifecycle: ctx, Calls: &agent.TaskCalls{}, Prepare: func(execution rpc.ApprovedExecution) (func(context.Context) error, error) { if handler == nil { return nil, errors.New("Agent session is unavailable") } delivery := &agent.RecordingDelivery{ Call: recordingClient(execution), Recovery: &agent.RecordingRecovery{ Root: settings.RecoveryRoot, Upload: agent.UploadClient{AllowedHosts: map[string]struct{}{strings.ToLower(settings.OSSAllowedHost): {}}}, }, Journal: &agent.ResultJournal{Root: settings.RecoveryRoot}, } return (&rpc.ApprovedRecordedRealCall{ Loader: loader, MediaPayloadType: 118, EvidenceRoot: settings.EvidenceRoot, Capture: settings.Capture, MaxWAVBytes: 64 << 20, ReportTimeout: 15 * time.Minute, Delivery: delivery, Mirror: mirror, }).Prepare(execution) }, OnFailure: func(execution rpc.ApprovedExecution, cause error) error { log.Printf("Agent real call failure: event_id=%q task_id=%q native_phase=%q ari_http_status=%d confirmed_preanswer_end=%t cause_type=%T", execution.SourceEventID, execution.TaskID, asterisk.NativeCallPhase(cause), asterisk.NativeCallHTTPStatus(cause), asterisk.NativeCallConfirmedPreAnswerEnd(cause), cause) if !asterisk.NativeCallNeverSubmitted(cause) { return nil } if err := reportDefiniteNonDialFailure(ctx, cause, recordingClient(execution)); err != nil { log.Printf("Agent definite pre-dial end unconfirmed: event_id=%q error_type=%T", execution.SourceEventID, err) return err } log.Printf("Agent definite pre-dial end confirmed: event_id=%q", execution.SourceEventID) return nil }, } handler, err = rpc.NewApprovedAgentServer(rpc.ServerOptions{ Mode: settings.Mode, StatePath: settings.SessionPath, ApprovedDispatcherID: settings.DispatcherID, Status: &agentpb.AgentStatus{AgentId: settings.AgentID, CellId: settings.CellID, BootId: uuid.NewString(), ProtocolVersion: "agent.v1"}, RequirePeerCertificate: true, PeerCertificateFingerprints: settings.PeerFingerprints, LoadedSIP: loader.LoadedSIP, ApplySIP: loader.Apply, }, worker) return handler, err }